tksbrokerapi.TKSBrokerAPI

TKSBrokerAPI is a python API to work with some methods of Tinkoff Open API using REST protocol. It can view history, orders and market information. Also, you can open orders and trades.

If you run this module as CLI program then it realizes simple logic: receiving a lot of options and execute one command. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples

Used constants are in the TKSEnums module: https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSEnums.html

About Tinkoff Invest API: https://tinkoff.github.io/investAPI/

Tinkoff Invest API documentation: https://tinkoff.github.io/investAPI/swagger-ui/

   1# -*- coding: utf-8 -*-
   2# Author: Timur Gilmullin
   3
   4"""
   5**TKSBrokerAPI** is a python API to work with some methods of Tinkoff Open API using REST protocol.
   6It can view history, orders and market information. Also, you can open orders and trades.
   7
   8If you run this module as CLI program then it realizes simple logic: receiving a lot of options and execute one command.
   9**See examples:** https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples
  10
  11**Used constants are in the TKSEnums module:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSEnums.html
  12
  13About Tinkoff Invest API: https://tinkoff.github.io/investAPI/
  14
  15Tinkoff Invest API documentation: https://tinkoff.github.io/investAPI/swagger-ui/
  16"""
  17
  18# Copyright (c) 2022 Gilmillin Timur Mansurovich
  19#
  20# Licensed under the Apache License, Version 2.0 (the "License");
  21# you may not use this file except in compliance with the License.
  22# You may obtain a copy of the License at
  23#
  24#     http://www.apache.org/licenses/LICENSE-2.0
  25#
  26# Unless required by applicable law or agreed to in writing, software
  27# distributed under the License is distributed on an "AS IS" BASIS,
  28# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  29# See the License for the specific language governing permissions and
  30# limitations under the License.
  31
  32
  33import sys
  34import os
  35from argparse import ArgumentParser
  36from importlib.metadata import version
  37
  38from datetime import datetime, timedelta
  39from dateutil.tz import tzlocal, tzutc
  40from time import sleep
  41
  42import re
  43import json
  44import requests
  45import traceback as tb
  46from typing import Union
  47
  48from multiprocessing import cpu_count
  49from multiprocessing.pool import ThreadPool
  50import pandas as pd
  51
  52from TKSEnums import *  # A lot of constants from enums sections: https://tinkoff.github.io/investAPI/swagger-ui/
  53
  54from pricegenerator.PriceGenerator import PriceGenerator, uLogger  # This module has a lot of instruments to work with candles data. See docs here: https://github.com/Tim55667757/PriceGenerator
  55from pricegenerator.UniLogger import DisableLogger as PGDisLog  # Method for disable log from PriceGenerator
  56
  57import UniLogger as uLog  # Logger for TKSBrokerAPI
  58
  59
  60# --- Common technical parameters:
  61
  62PGDisLog(uLogger.handlers[0])  # Disable 3-rd party logging from PriceGenerator
  63uLogger = uLog.UniLogger  # init logger for TKSBrokerAPI
  64uLogger.level = 10  # debug level by default for TKSBrokerAPI module
  65uLogger.handlers[0].level = 20  # info level by default for STDOUT of TKSBrokerAPI module
  66
  67__version__ = "1.5"  # The "major.minor" version setup here, but build number define at the build-server only
  68
  69CPU_COUNT = cpu_count()  # host's real CPU count
  70CPU_USAGES = CPU_COUNT - 1 if CPU_COUNT > 1 else 1  # how many CPUs will be used for parallel calculations
  71
  72# --- Main constants:
  73
  74NANO = 0.000000001  # SI-constant nano = 10^-9
  75
  76
  77def NanoToFloat(units: str, nano: int) -> float:
  78    """
  79    Convert number in nano-view mode with string parameter `units` and integer parameter `nano` to float view. Examples:
  80
  81    `NanoToFloat(units="2", nano=500000000) -> 2.5`
  82
  83    `NanoToFloat(units="0", nano=50000000) -> 0.05`
  84
  85    :param units: integer string or integer parameter that represents the integer part of number
  86    :param nano: integer string or integer parameter that represents the fractional part of number
  87    :return: float view of number
  88    """
  89    return int(units) + int(nano) * NANO
  90
  91
  92def FloatToNano(number: float) -> dict:
  93    """
  94    Convert float number to nano-type view: dictionary with string `units` and integer `nano` parameters `{"units": "string", "nano": integer}`. Examples:
  95
  96    `FloatToNano(number=2.5) -> {"units": "2", "nano": 500000000}`
  97
  98    `FloatToNano(number=0.05) -> {"units": "0", "nano": 50000000}`
  99
 100    :param number: float number
 101    :return: nano-type view of number: `{"units": "string", "nano": integer}`
 102    """
 103    splitByPoint = str(number).split(".")
 104    frac = 0
 105
 106    if len(splitByPoint) > 1:
 107        if len(splitByPoint[1]) <= 9:
 108            frac = int("{}{}".format(
 109                int(splitByPoint[1]),
 110                "0" * (9 - len(splitByPoint[1])),
 111            ))
 112
 113    if (number < 0) and (frac > 0):
 114        frac = -frac
 115
 116    return {"units": str(int(number)), "nano": frac}
 117
 118
 119def GetDatesAsString(start: str = None, end: str = None) -> tuple:
 120    """
 121    Create tuple of date and time strings with timezone parsed from user-friendly date.
 122
 123    User dates format must be like: `%Y-%m-%d`, e.g. `2020-02-03` (3 Feb, 2020).
 124
 125    Example input: "2022-06-01" "2022-06-20" -> output: ("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z")
 126    An error exception will occur if input date has incorrect format.
 127
 128    If `start=None`, `end=None` then return dates from yesterday to the end of the day.
 129    If `start=some_date_1`, `end=None` then return dates from `some_date_1` to the end of the day.
 130    If `start=some_date_1`, `end=some_date_2` then return dates from start of `some_date_1` to end of `some_date_2`.
 131    Start day may be negative integer numbers: `-1`, `-2`, `-3` - how many days ago.
 132
 133    Also, you can use keywords for start if `end=None`:
 134    `today` (from 00:00:00 to the end of current day),
 135    `yesterday` (-1 day from 00:00:00 to 23:59:59),
 136    `week` (-7 day from 00:00:00 to the end of current day),
 137    `month` (-30 day from 00:00:00 to the end of current day),
 138    `year` (-365 day from 00:00:00 to the end of current day),
 139
 140    :return: tuple with 2 strings `(start, end)` dates in UTC ISO time format `%Y-%m-%dT%H:%M:%SZ` for OpenAPI.
 141             See date and time format here: `TKSEnums.TKS_DATE_TIME_FORMAT`.
 142             Example: `("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z")`. Second string is the end of the last day.
 143    """
 144    uLogger.debug("Input start day is [{}] (UTC), end day is [{}] (UTC)".format(start, end))
 145    s = datetime.now(tzutc()).replace(hour=0, minute=0, second=0, microsecond=0)  # start of the current day
 146    e = s.replace(hour=23, minute=59, second=59, microsecond=0)  # end of the current day
 147
 148    # time between start and the end of the current day:
 149    if start is None or start.lower() == "today":
 150        pass
 151
 152    # from start of the last day to the end of the last day:
 153    elif start.lower() == "yesterday":
 154        s -= timedelta(days=1)
 155        e -= timedelta(days=1)
 156
 157    # week (-7 day from 00:00:00 to the end of the current day):
 158    elif start.lower() == "week":
 159        s -= timedelta(days=6)  # +1 current day already taken into account
 160
 161    # month (-30 day from 00:00:00 to the end of current day):
 162    elif start.lower() == "month":
 163        s -= timedelta(days=29)  # +1 current day already taken into account
 164
 165    # year (-365 day from 00:00:00 to the end of current day):
 166    elif start.lower() == "year":
 167        s -= timedelta(days=364)  # +1 current day already taken into account
 168
 169    # -N days ago to the end of current day:
 170    elif start.startswith('-') and start[1:].isdigit():
 171        s -= timedelta(days=abs(int(start)) - 1)  # +1 current day already taken into account
 172
 173    # dates between start day at 00:00:00 and the end of the last day at 23:59:59:
 174    else:
 175        s = datetime.strptime(start, "%Y-%m-%d").replace(hour=0, minute=0, second=0, microsecond=0, tzinfo=tzutc())
 176        e = datetime.strptime(end, "%Y-%m-%d").replace(hour=23, minute=59, second=59, microsecond=0, tzinfo=tzutc()) if end is not None else e
 177
 178    # converting to UTC ISO time formatted with Z suffix for Tinkoff Open API:
 179    s = s.strftime(TKS_DATE_TIME_FORMAT)
 180    e = e.strftime(TKS_DATE_TIME_FORMAT)
 181
 182    uLogger.debug("Start day converted to UTC ISO format, with Z: [{}], and the end day: [{}]".format(s, e))
 183
 184    return s, e
 185
 186
 187class TinkoffBrokerServer:
 188    """
 189    This class implements methods to work with Tinkoff broker server.
 190
 191    Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/
 192
 193    About `token`: https://tinkoff.github.io/investAPI/token/
 194    """
 195    def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None:
 196        """
 197        Main class init.
 198
 199        :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`.
 200        :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
 201                          Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.
 202        :param useCache: use default cache file with raw data to use instead of `iList`.
 203                         True by default. Cache is auto-update if new day has come.
 204                         If you don't want to use cache and always updates raw data then set `useCache=False`.
 205        :param defaultCache: path to default cache file. `dump.json` by default.
 206        """
 207        if token is None or not token:
 208            try:
 209                self.token = r"{}".format(os.environ["TKS_API_TOKEN"])
 210                uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/")
 211
 212            except KeyError:
 213                uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/")
 214                raise Exception("Token required")
 215
 216        else:
 217            self.token = token  # highly priority than environment variable 'TKS_API_TOKEN'
 218            uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`")
 219
 220        if accountId is None or not accountId:
 221            try:
 222                self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"])
 223                uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId))
 224
 225            except KeyError:
 226                uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).")
 227
 228        else:
 229            self.accountId = accountId  # highly priority than environment variable 'TKS_ACCOUNT_ID'
 230            uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId))
 231
 232        self.version = __version__  # duplicate here used TKSBrokerAPI main version
 233        """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
 234
 235        Latest version: https://pypi.org/project/tksbrokerapi/
 236        """
 237
 238        self.aliases = TKS_TICKER_ALIASES
 239        """Some aliases instead official tickers.
 240
 241        See also: `TKSEnums.TKS_TICKER_ALIASES`
 242        """
 243
 244        self.aliasesKeys = self.aliases.keys()  # re-calc only first time at class init
 245
 246        self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED  # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there
 247
 248        self.ticker = ""
 249        """String with ticker, e.g. `GOOGL`. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 250
 251        See also: `SearchByTicker()`, `SearchInstruments()`.
 252        """
 253
 254        self.figi = ""
 255        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`.
 256
 257        See also: `SearchByFIGI()`, `SearchInstruments()`.
 258        """
 259
 260        self.depth = 1
 261        """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI.
 262
 263        See also: `GetCurrentPrices()`.
 264        """
 265
 266        self.server = r"https://invest-public-api.tinkoff.ru/rest"
 267        """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
 268
 269        See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`.
 270        """
 271
 272        uLogger.debug("Broker API server: {}".format(self.server))
 273
 274        self.timeout = 15
 275        """Server operations timeout in seconds. Default: `15`.
 276
 277        See also: `SendAPIRequest()`.
 278        """
 279
 280        self.headers = {
 281            "Content-Type": "application/json",
 282            "accept": "application/json",
 283            "Authorization": "Bearer {}".format(self.token),
 284            "x-app-name": "Tim55667757.TKSBrokerAPI",
 285        }
 286        """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`.
 287
 288        See also: `SendAPIRequest()`.
 289        """
 290
 291        self.body = None
 292        """Request body which send to broker server. Default: `None`.
 293
 294        See also: `SendAPIRequest()`.
 295        """
 296
 297        self.historyFile = None
 298        """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only pandas dataframe.
 299
 300        See also: `History()`.
 301        """
 302
 303        self.htmlHistoryFile = "index.html"
 304        """Full path to the html file where rendered candles chart stored. Default: `index.html`.
 305
 306        See also: `ShowHistoryChart()`.
 307        """
 308
 309        self.instrumentsFile = "instruments.md"
 310        """Filename where full available to user instruments list will be saved. Default: `instruments.md`.
 311
 312        See also: `ShowInstrumentsInfo()`.
 313        """
 314
 315        self.searchResultsFile = "search-results.md"
 316        """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`.
 317
 318        See also: `SearchInstruments()`.
 319        """
 320
 321        self.pricesFile = "prices.md"
 322        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 323
 324        See also: `GetListOfPrices()`.
 325        """
 326
 327        self.infoFile = "info.md"
 328        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 329
 330        See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`.
 331        """
 332
 333        self.bondsXLSXFile = "ext-bonds.xlsx"
 334        """Filename where wider pandas dataframe with more information about bonds: main info, current prices, 
 335        bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`.
 336
 337        See also: `ExtendBondsData()`.
 338        """
 339
 340        self.calendarFile = "calendar.md"
 341        """Filename where bonds payment calendar will be saved. Default: `calendar.md`.
 342        
 343        Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`.
 344
 345        See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`.
 346        """
 347
 348        self.overviewFile = "overview.md"
 349        """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`.
 350
 351        See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`.
 352        """
 353
 354        self.overviewDigestFile = "overview-digest.md"
 355        """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`.
 356
 357        See also: `Overview()` with parameter `details="digest"`.
 358        """
 359
 360        self.overviewPositionsFile = "overview-positions.md"
 361        """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`.
 362
 363        See also: `Overview()` with parameter `details="positions"`.
 364        """
 365
 366        self.overviewOrdersFile = "overview-orders.md"
 367        """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`.
 368
 369        See also: `Overview()` with parameter `details="orders"`.
 370        """
 371
 372        self.overviewAnalyticsFile = "overview-analytics.md"
 373        """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`.
 374
 375        See also: `Overview()` with parameter `details="analytics"`.
 376        """
 377
 378        self.reportFile = "deals.md"
 379        """Filename where history of deals and trade statistics will be saved. Default: `deals.md`.
 380
 381        See also: `Deals()`.
 382        """
 383
 384        self.withdrawalLimitsFile = "limits.md"
 385        """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`.
 386
 387        See also: `OverviewLimits()` and `RequestLimits()`.
 388        """
 389
 390        self.userInfoFile = "user-info.md"
 391        """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`.
 392
 393        See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`.
 394        """
 395
 396        self.userAccountsFile = "accounts.md"
 397        """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`.
 398
 399        See also: `OverviewAccounts()`, `RequestAccounts()`.
 400        """
 401
 402        self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache
 403        """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`.
 404
 405        Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`.
 406
 407        See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`.
 408        """
 409
 410        self.iList = None  # init iList for raw instruments data
 411        """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`.
 412        
 413        See also: `Listing()`, `DumpInstruments()`.
 414        """
 415
 416        # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server:
 417        if useCache:
 418            if os.path.exists(self.iListDumpFile):
 419                dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc())  # dump modification date and time
 420                curTime = datetime.now(tzutc())
 421
 422                if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year):
 423                    uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
 424
 425                    self.DumpInstruments(forceUpdate=True)  # updating self.iList and dump file
 426
 427                else:
 428                    self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8"))  # load iList from dump
 429
 430                    uLogger.debug("Local cache with raw instruments data is used: [{}]".format(os.path.abspath(self.iListDumpFile)))
 431                    uLogger.debug("Dump file was last modified [{}] UTC".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
 432
 433            else:
 434                uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...")
 435                self.DumpInstruments(forceUpdate=True)  # updating self.iList and creating default dump file
 436
 437        else:
 438            self.iList = self.Listing()  # request new raw instruments data from broker server
 439            self.DumpInstruments(forceUpdate=False)  # save raw instrument's data to default dump file `iListDumpFile`
 440
 441        self.priceModel = PriceGenerator()  # init PriceGenerator object to work with candles data
 442        """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
 443
 444        See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
 445        """
 446
 447    @staticmethod
 448    def _ParseJSON(rawData="{}", debug: bool = False) -> dict:
 449        """
 450        Parse JSON from response string.
 451
 452        :param rawData: this is a string with JSON-formatted text.
 453        :param debug: if `True` then print more debug information.
 454        :return: JSON (dictionary), parsed from server response string.
 455        """
 456        if debug:
 457            uLogger.debug("Raw text body:")
 458            uLogger.debug(rawData)
 459
 460        responseJSON = json.loads(rawData) if rawData else {}
 461
 462        if debug:
 463            uLogger.debug("JSON formatted:")
 464            for jsonLine in json.dumps(responseJSON, indent=4).split('\n'):
 465                uLogger.debug(jsonLine)
 466
 467        return responseJSON
 468
 469    def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5, debug: bool = False) -> dict:
 470        """
 471        Send GET or POST request to broker server and receive JSON object.
 472
 473        self.header: must be defining with dictionary of headers.
 474        self.body: if define then used as request body. None by default.
 475        self.timeout: global request timeout, 15 seconds by default.
 476        :param url: url with REST request.
 477        :param reqType: send "GET" or "POST" request. "GET" by default.
 478        :param retry: how many times retry after first request if an 5xx server errors occurred.
 479        :param pause: sleep time in seconds between retries.
 480        :param debug: if `True` then print more debug information, e.g. request and response parameters, headers etc.
 481        :return: response JSON (dictionary) from broker.
 482        """
 483        if reqType not in ("GET", "POST"):
 484            uLogger.error("You can define request type: 'GET' or 'POST'!")
 485            raise Exception("Incorrect value")
 486
 487        if debug:
 488            uLogger.debug("Request parameters:")
 489            uLogger.debug("    - REST API URL: {}".format(url))
 490            uLogger.debug("    - request type: {}".format(reqType))
 491            uLogger.debug("    - headers: {}".format(str(self.headers).replace(self.token, "*** request token ***")))
 492            uLogger.debug("    - body: {}".format(self.body))
 493
 494        # fast hack to avoid all operations with some tickers/FIGI
 495        responseJSON = {}
 496        oK = True
 497        for item in self.exclude:
 498            if item in url:
 499                if debug:
 500                    uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude)))
 501
 502                oK = False
 503                break
 504
 505        if oK:
 506            counter = 0
 507            response = None
 508            errMsg = ""
 509
 510            while not response and counter <= retry:
 511                if reqType == "GET":
 512                    response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout)
 513
 514                if reqType == "POST":
 515                    response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout)
 516
 517                if debug:
 518                    uLogger.debug("Response:")
 519                    uLogger.debug("    - status code: {}".format(response.status_code))
 520                    uLogger.debug("    - reason: {}".format(response.reason))
 521                    uLogger.debug("    - body length: {}".format(len(response.text)))
 522                    uLogger.debug("    - headers: {}".format(response.headers))
 523
 524                # Server returns some headers:
 525                # - `x-ratelimit-limit` - shows the settings of the current user limit for this method.
 526                # - `x-ratelimit-remaining` - the number of remaining requests of this type per minute.
 527                # - `x-ratelimit-reset` - time in seconds before resetting the request counter.
 528                # See: https://tinkoff.github.io/investAPI/grpc/#kreya
 529                if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0":
 530                    rateLimitWait = int(response.headers["x-ratelimit-reset"])
 531                    uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait))
 532                    sleep(rateLimitWait)
 533
 534                # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
 535                if 400 <= response.status_code < 500:
 536                    msg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 537                    uLogger.debug("    - not oK, but do not retry for 4xx errors, {}".format(msg))
 538                    counter = retry + 1
 539
 540                if 500 <= response.status_code < 600:
 541                    errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 542                    uLogger.debug("    - not oK, {}".format(errMsg))
 543                    counter += 1
 544
 545                    if counter <= retry:
 546                        uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause))
 547                        sleep(pause)
 548
 549            responseJSON = self._ParseJSON(response.text)
 550
 551            if errMsg:
 552                uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/")
 553                uLogger.error("    - not oK, {}".format(errMsg))
 554
 555        return responseJSON
 556
 557    def _IUpdater(self, iType: str) -> tuple:
 558        """
 559        Request instrument by type from server. See available API methods for instruments:
 560        Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies
 561        Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares
 562        Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds
 563        Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs
 564        Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures
 565
 566        :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 567        :return: tuple with iType name and list of available instruments of current type for defined user token.
 568        """
 569        result = []
 570
 571        if iType in TKS_INSTRUMENTS:
 572            uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType))
 573
 574            # all instruments have the same body in API v2 requests:
 575            self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"})  # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL]
 576            instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType)
 577            result = self.SendAPIRequest(instrumentURL, reqType="POST", debug=False)["instruments"]
 578
 579        return iType, result
 580
 581    def _IWrapper(self, kwargs):
 582        """
 583        Wrapper runs instrument's update method `_IUpdater()`.
 584        It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206
 585        """
 586        return self._IUpdater(**kwargs)
 587
 588    def Listing(self) -> dict:
 589        """
 590        Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
 591
 592        :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
 593        """
 594        uLogger.debug("Requesting all available instruments for current account. Wait, please...")
 595        uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES))
 596
 597        # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService
 598        # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 599        iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS]
 600
 601        poolUpdater = ThreadPool(processes=CPU_USAGES)  # create pool for update instruments in parallel mode
 602        listing = poolUpdater.map(self._IWrapper, iParams)  # execute update operations
 603        poolUpdater.close()
 604
 605        # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures.
 606        # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method
 607        iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing}
 608
 609        # calculate minimum price increment (step) for all instruments and set up instrument's type:
 610        for iType in iList.keys():
 611            for ticker in iList[iType]:
 612                iList[iType][ticker]["type"] = iType
 613
 614                if "minPriceIncrement" in iList[iType][ticker].keys():
 615                    iList[iType][ticker]["step"] = NanoToFloat(
 616                        iList[iType][ticker]["minPriceIncrement"]["units"],
 617                        iList[iType][ticker]["minPriceIncrement"]["nano"],
 618                    )
 619
 620                else:
 621                    iList[iType][ticker]["step"] = 0  # hack to avoid empty value in some instruments, e.g. futures
 622
 623        return iList
 624
 625    def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
 626        """
 627        Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
 628
 629        See also: `DumpInstruments()`, `Listing()`.
 630
 631        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 632                            otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) .
 633        """
 634        if self.iListDumpFile is None or not self.iListDumpFile:
 635            uLogger.error("Output name of dump file must be defined!")
 636            raise Exception("Filename required")
 637
 638        if not self.iList or forceUpdate:
 639            self.iList = self.Listing()
 640
 641        xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx"
 642
 643        # Save as XLSX with separated sheets for every type of instruments:
 644        with pd.ExcelWriter(
 645                path=xlsxDumpFile,
 646                date_format=TKS_DATE_FORMAT,
 647                datetime_format=TKS_DATE_TIME_FORMAT,
 648                mode="w",
 649        ) as writer:
 650            for iType in TKS_INSTRUMENTS:
 651                df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index")  # generate pandas object from self.iList dictionary
 652                df = df[sorted(df)]  # sorted by column names
 653                df = df.applymap(
 654                    lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item,
 655                    na_action="ignore",
 656                )  # converting numbers from nano-type to float in every cell
 657                df.to_excel(
 658                    writer,
 659                    sheet_name=iType,
 660                    encoding="UTF-8",
 661                    freeze_panes=(1, 1),
 662                )  # saving as XLSX-file with freeze first row and column as headers
 663
 664        uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))
 665
 666    def DumpInstruments(self, forceUpdate: bool = True) -> str:
 667        """
 668        Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
 669        using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file.
 670
 671        See also: `DumpInstrumentsAsXLSX()`, `Listing()`.
 672
 673        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 674                            otherwise just saves exist `iList` as JSON-file (default: `dump.json`).
 675        :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file.
 676        """
 677        if self.iListDumpFile is None or not self.iListDumpFile:
 678            uLogger.error("Output name of dump file must be defined!")
 679            raise Exception("Filename required")
 680
 681        if not self.iList or forceUpdate:
 682            self.iList = self.Listing()
 683
 684        jsonDump = json.dumps(self.iList, indent=4, sort_keys=False)  # create JSON object as string
 685        with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH:
 686            fH.write(jsonDump)
 687
 688        uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile)))
 689
 690        return jsonDump
 691
 692    def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str:
 693        """
 694        Show information about one instrument defined by json data and prints it in Markdown format.
 695
 696        See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`.
 697
 698        :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self.ticker]`
 699        :param show: if `True` then also printing information about instrument and its current price.
 700        :return: multilines text in Markdown format with information about one instrument.
 701        """
 702        splitLine = "|                                                             |                                                        |\n"
 703        infoText = ""
 704
 705        if iJSON is not None and iJSON and isinstance(iJSON, dict):
 706            info = [
 707                "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]),
 708                "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
 709                "| Parameters                                                  | Values                                                 |\n",
 710                "|-------------------------------------------------------------|--------------------------------------------------------|\n",
 711                "| Ticker:                                                     | {:<54} |\n".format(iJSON["ticker"]),
 712                "| Full name:                                                  | {:<54} |\n".format(iJSON["name"]),
 713            ]
 714
 715            if "sector" in iJSON.keys() and iJSON["sector"]:
 716                info.append("| Sector:                                                     | {:<54} |\n".format(iJSON["sector"]))
 717
 718            info.append("| Country of instrument:                                      | {:<54} |\n".format("{}{}".format(
 719                "({}) ".format(iJSON["countryOfRisk"]) if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] else "",
 720                iJSON["countryOfRiskName"] if "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"] else "",
 721            )))
 722
 723            info.extend([
 724                splitLine,
 725                "| FIGI (Financial Instrument Global Identifier):              | {:<54} |\n".format(iJSON["figi"]),
 726                "| Exchange:                                                   | {:<54} |\n".format(iJSON["exchange"]),
 727            ])
 728
 729            if "isin" in iJSON.keys() and iJSON["isin"]:
 730                info.append("| ISIN (International Securities Identification Number):      | {:<54} |\n".format(iJSON["isin"]))
 731
 732            if "classCode" in iJSON.keys():
 733                info.append("| Class Code (exchange section where instrument is traded):   | {:<54} |\n".format(iJSON["classCode"]))
 734
 735            info.extend([
 736                splitLine,
 737                "| Current broker security trading status:                     | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]),
 738                splitLine,
 739                "| Buy operations allowed:                                     | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"),
 740                "| Sale operations allowed:                                    | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"),
 741                "| Short positions allowed:                                    | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"),
 742            ])
 743
 744            if iJSON["figi"]:
 745                self.figi = iJSON["figi"]
 746                iJSON = iJSON | self.RequestTradingStatus()
 747
 748                info.extend([
 749                    splitLine,
 750                    "| Limit orders allowed:                                       | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"),
 751                    "| Market orders allowed:                                      | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"),
 752                    "| API trade allowed:                                          | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"),
 753                ])
 754
 755            info.append(splitLine)
 756
 757            if "type" in iJSON.keys() and iJSON["type"]:
 758                info.append("| Type of the instrument:                                     | {:<54} |\n".format(iJSON["type"]))
 759
 760            if "futuresType" in iJSON.keys() and iJSON["futuresType"]:
 761                info.append("| Futures type:                                               | {:<54} |\n".format(iJSON["futuresType"]))
 762
 763            if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]:
 764                info.append("| IPO date:                                                   | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", "")))
 765
 766            if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]:
 767                info.append("| Released date:                                              | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", "")))
 768
 769            if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]:
 770                info.append("| Rebalancing frequency:                                      | {:<54} |\n".format(iJSON["rebalancingFreq"]))
 771
 772            if "focusType" in iJSON.keys() and iJSON["focusType"]:
 773                info.append("| Focusing type:                                              | {:<54} |\n".format(iJSON["focusType"]))
 774
 775            if "assetType" in iJSON.keys() and iJSON["assetType"]:
 776                info.append("| Asset type:                                                 | {:<54} |\n".format(iJSON["assetType"]))
 777
 778            if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]:
 779                info.append("| Basic asset:                                                | {:<54} |\n".format(iJSON["basicAsset"]))
 780
 781            if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]:
 782                info.append("| Basic asset size:                                           | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"]))))
 783
 784            if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]:
 785                info.append("| ISO currency name:                                          | {:<54} |\n".format(iJSON["isoCurrencyName"]))
 786
 787            if "currency" in iJSON.keys():
 788                info.append("| Payment currency:                                           | {:<54} |\n".format(iJSON["currency"]))
 789
 790            if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys():
 791                info.append("| Nominal currency:                                           | {:<54} |\n".format(iJSON["nominal"]["currency"]))
 792
 793            if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]:
 794                info.append("| First trade date:                                           | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", "")))
 795
 796            if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]:
 797                info.append("| Last trade date:                                            | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", "")))
 798
 799            if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]:
 800                info.append("| Date of expiration:                                         | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", "")))
 801
 802            if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]:
 803                info.append("| State registration date:                                    | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", "")))
 804
 805            if "placementDate" in iJSON.keys() and iJSON["placementDate"]:
 806                info.append("| Placement date:                                             | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", "")))
 807
 808            if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]:
 809                info.append("| Maturity date:                                              | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", "")))
 810
 811            if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]:
 812                info.append("| Perpetual bond:                                             | Yes                                                    |\n")
 813
 814            if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]:
 815                info.append("| Over-the-counter (OTC) securities:                          | Yes                                                    |\n")
 816
 817            iExt = None
 818            if iJSON["type"] == "Bonds":
 819                info.extend([
 820                    splitLine,
 821                    "| Bond issue (size / plan):                                   | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])),
 822                    "| Nominal price (100%):                                       | {:<54} |\n".format("{} {}".format(
 823                        "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."),
 824                        iJSON["nominal"]["currency"],
 825                    )),
 826                ])
 827
 828                if "floatingCouponFlag" in iJSON.keys():
 829                    info.append("| Floating coupon:                                            | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No"))
 830
 831                if "amortizationFlag" in iJSON.keys():
 832                    info.append("| Amortization:                                               | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No"))
 833
 834                info.append(splitLine)
 835
 836                if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]:
 837                    info.append("| Number of coupon payments per year:                         | {:<54} |\n".format(iJSON["couponQuantityPerYear"]))
 838
 839                iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False)  # extended bonds data
 840
 841                info.extend([
 842                    "| Days last to maturity date:                                 | {:<54} |\n".format(iExt["daysToMaturity"][0]),
 843                    "| Coupons yield (average coupon daily yield * 365):           | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])),
 844                    "| Current price yield (average daily yield * 365):            | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])),
 845                ])
 846
 847                if "aciValue" in iJSON.keys() and iJSON["aciValue"]:
 848                    info.append("| Current accumulated coupon income (ACI):                    | {:<54} |\n".format("{:.2f} {}".format(
 849                        NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]),
 850                        iJSON["aciValue"]["currency"]
 851                    )))
 852
 853            if "currentPrice" in iJSON.keys():
 854                info.append(splitLine)
 855
 856                currency = iJSON["currency"] if "currency" in iJSON.keys() else ""  # nominal currency for bonds, otherwise currency of instrument
 857                aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else ""  # payment currency
 858
 859                bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0  # previous close price of bond
 860                bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0  # last price of bond
 861                bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0  # max price of bond
 862                bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0  # min price of bond
 863                bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0  # delta between last deal price and last close
 864
 865                curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0
 866                curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0
 867
 868                info.extend([
 869                    "| Previous close price of the instrument:                     | {:<54} |\n".format("{}{}".format(
 870                        "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A",
 871                        "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 872                    )),
 873                    "| Last deal price of the instrument:                          | {:<54} |\n".format("{}{}".format(
 874                        "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A",
 875                        "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 876                    )),
 877                    "| Changes between last deal price and last close              | {:<54} |\n".format(
 878                        "{:.2f}%{}".format(
 879                            iJSON["currentPrice"]["changes"],
 880                            " ({}{:.2f} {})".format(
 881                                "+" if bondChangesDelta > 0 else "",
 882                                bondChangesDelta,
 883                                aciCurrency
 884                            ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format(
 885                                "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "",
 886                                iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"],
 887                                currency
 888                            ),
 889                        )
 890                    ),
 891                    "| Current limit price, min / max:                             | {:<54} |\n".format("{}{} / {}{}{}".format(
 892                        "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A",
 893                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 894                        "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A",
 895                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 896                        " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else ""
 897                    )),
 898                    "| Actual price, sell / buy:                                   | {:<54} |\n".format("{}{} / {}{}{}".format(
 899                        "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A",
 900                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 901                        "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A",
 902                        "%" if iJSON["type"] == "Bonds" else" {}".format(currency),
 903                        " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else ""
 904                    )),
 905                ])
 906
 907            if "lot" in iJSON.keys():
 908                info.append("| Minimum lot to buy:                                         | {:<54} |\n".format(iJSON["lot"]))
 909
 910            if "step" in iJSON.keys() and iJSON["step"] != 0:
 911                info.append("| Minimum price increment (step):                             | {:<54} |\n".format(iJSON["step"]))
 912
 913            # Add bond payment calendar:
 914            if iJSON["type"] == "Bonds":
 915                strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False)   # bond payment calendar
 916                info.extend(["\n", strCalendar])
 917
 918            infoText += "".join(info)
 919
 920            if show:
 921                uLogger.info("{}".format(infoText))
 922
 923            else:
 924                uLogger.debug("{}".format(infoText))
 925
 926            if self.infoFile is not None:
 927                with open(self.infoFile, "w", encoding="UTF-8") as fH:
 928                    fH.write(infoText)
 929
 930                uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile)))
 931
 932        return infoText
 933
 934    def SearchByTicker(self, requestPrice: bool = False, show: bool = False, debug: bool = False) -> dict:
 935        """
 936        Search and return raw broker's information about instrument by its ticker.
 937        `ticker` must be defined! If debug=True then print all debug messages.
 938
 939        :param requestPrice: if `False` then do not request current price of instrument (because this is long operation).
 940        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 941        :param debug: if `True` then print all debug console messages.
 942        :return: JSON formatted data with information about instrument.
 943        """
 944        tickerJSON = {}
 945        if debug:
 946            uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self.ticker))
 947
 948        if not self.ticker:
 949            uLogger.warning("self.ticker variable is not be empty!")
 950
 951        else:
 952            if self.ticker in TKS_TICKERS_OR_FIGI_EXCLUDED:
 953                uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self.ticker))
 954                raise Exception("Instrument not allowed")
 955
 956            if not self.iList:
 957                self.iList = self.Listing()
 958
 959            if self.ticker in self.iList["Shares"].keys():
 960                tickerJSON = self.iList["Shares"][self.ticker]
 961                if debug:
 962                    uLogger.debug("Ticker [{}] found in shares list".format(self.ticker))
 963
 964            elif self.ticker in self.iList["Currencies"].keys():
 965                tickerJSON = self.iList["Currencies"][self.ticker]
 966                if debug:
 967                    uLogger.debug("Ticker [{}] found in currencies list".format(self.ticker))
 968
 969            elif self.ticker in self.iList["Bonds"].keys():
 970                tickerJSON = self.iList["Bonds"][self.ticker]
 971                if debug:
 972                    uLogger.debug("Ticker [{}] found in bonds list".format(self.ticker))
 973
 974            elif self.ticker in self.iList["Etfs"].keys():
 975                tickerJSON = self.iList["Etfs"][self.ticker]
 976                if debug:
 977                    uLogger.debug("Ticker [{}] found in etfs list".format(self.ticker))
 978
 979            elif self.ticker in self.iList["Futures"].keys():
 980                tickerJSON = self.iList["Futures"][self.ticker]
 981                if debug:
 982                    uLogger.debug("Ticker [{}] found in futures list".format(self.ticker))
 983
 984        if tickerJSON:
 985            self.figi = tickerJSON["figi"]
 986
 987            if requestPrice:
 988                tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False)
 989
 990                if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None:
 991                    tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"]
 992
 993                else:
 994                    tickerJSON["currentPrice"]["changes"] = 0
 995
 996            if show:
 997                self.ShowInstrumentInfo(iJSON=tickerJSON, show=True)  # print info as Markdown text
 998
 999        else:
1000            if show:
1001                uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self.ticker))
1002
1003        return tickerJSON
1004
1005    def SearchByFIGI(self, requestPrice: bool = False, show: bool = False, debug: bool = False) -> dict:
1006        """
1007        Search and return raw broker's information about instrument by its FIGI.
1008        `figi` must be defined! If debug=True then print all debug messages.
1009
1010        :param requestPrice: if `False` then do not request current price of instrument (it's long operation).
1011        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
1012        :param debug: if `True` then print all debug console messages.
1013        :return: JSON formatted data with information about instrument.
1014        """
1015        figiJSON = {}
1016        if debug:
1017            uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self.figi))
1018
1019        if not self.figi:
1020            uLogger.warning("self.figi variable is not be empty!")
1021
1022        else:
1023            if self.figi in TKS_TICKERS_OR_FIGI_EXCLUDED:
1024                uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self.figi))
1025                raise Exception("Instrument not allowed")
1026
1027            if not self.iList:
1028                self.iList = self.Listing()
1029
1030            for item in self.iList["Shares"].keys():
1031                if self.figi == self.iList["Shares"][item]["figi"]:
1032                    figiJSON = self.iList["Shares"][item]
1033
1034                    if debug:
1035                        uLogger.debug("FIGI [{}] found in shares list".format(self.figi))
1036
1037                    break
1038
1039            if not figiJSON:
1040                for item in self.iList["Currencies"].keys():
1041                    if self.figi == self.iList["Currencies"][item]["figi"]:
1042                        figiJSON = self.iList["Currencies"][item]
1043
1044                        if debug:
1045                            uLogger.debug("FIGI [{}] found in currencies list".format(self.figi))
1046
1047                        break
1048
1049            if not figiJSON:
1050                for item in self.iList["Bonds"].keys():
1051                    if self.figi == self.iList["Bonds"][item]["figi"]:
1052                        figiJSON = self.iList["Bonds"][item]
1053
1054                        if debug:
1055                            uLogger.debug("FIGI [{}] found in bonds list".format(self.figi))
1056
1057                        break
1058
1059            if not figiJSON:
1060                for item in self.iList["Etfs"].keys():
1061                    if self.figi == self.iList["Etfs"][item]["figi"]:
1062                        figiJSON = self.iList["Etfs"][item]
1063
1064                        if debug:
1065                            uLogger.debug("FIGI [{}] found in etfs list".format(self.figi))
1066
1067                        break
1068
1069            if not figiJSON:
1070                for item in self.iList["Futures"].keys():
1071                    if self.figi == self.iList["Futures"][item]["figi"]:
1072                        figiJSON = self.iList["Futures"][item]
1073
1074                        if debug:
1075                            uLogger.debug("FIGI [{}] found in futures list".format(self.figi))
1076
1077                        break
1078
1079        if figiJSON:
1080            self.figi = figiJSON["figi"]
1081            self.ticker = figiJSON["ticker"]
1082
1083            if requestPrice:
1084                figiJSON["currentPrice"] = self.GetCurrentPrices(show=False)
1085
1086                if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None:
1087                    figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"]
1088
1089                else:
1090                    figiJSON["currentPrice"]["changes"] = 0
1091
1092            if show:
1093                self.ShowInstrumentInfo(iJSON=figiJSON, show=True)  # print info as Markdown text
1094
1095        else:
1096            if show:
1097                uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self.figi))
1098
1099        return figiJSON
1100
1101    def GetCurrentPrices(self, show: bool = True) -> dict:
1102        """
1103        Get and show Depth of Market with current prices of the instrument. If an error occurred then returns an empty record:
1104        `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`.
1105
1106        See also:
1107
1108        :param show: if `True` then print DOM to log and console.
1109        :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`.
1110        """
1111        prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0}
1112
1113        if self.depth < 1:
1114            uLogger.error("Depth of Market (DOM) must be >=1!")
1115            raise Exception("Incorrect value")
1116
1117        if not (self.ticker or self.figi):
1118            uLogger.error("self.ticker or self.figi variables must be defined!")
1119            raise Exception("Ticker or FIGI required")
1120
1121        if self.ticker and not self.figi:
1122            instrumentByTicker = self.SearchByTicker(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1123            self.figi = instrumentByTicker["figi"] if instrumentByTicker else ""
1124
1125        if not self.ticker and self.figi:
1126            instrumentByFigi = self.SearchByFIGI(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1127            self.ticker = instrumentByFigi["ticker"] if instrumentByFigi else ""
1128
1129        if not self.figi:
1130            uLogger.error("FIGI is not defined!")
1131            raise Exception("Ticker or FIGI required")
1132
1133        else:
1134            uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self.ticker, self.figi))
1135
1136            # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1137            priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook"
1138            self.body = str({"figi": self.figi, "depth": self.depth})
1139            pricesResponse = self.SendAPIRequest(priceURL, reqType="POST")
1140
1141            if pricesResponse:
1142                # list of dicts with sellers orders:
1143                prices["buy"] = [{"price": NanoToFloat(item["price"]["units"], item["price"]["nano"]), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]]
1144
1145                # list of dicts with buyers orders:
1146                prices["sell"] = [{"price": NanoToFloat(item["price"]["units"], item["price"]["nano"]), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]]
1147
1148                # max price of instrument at this time:
1149                prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None
1150
1151                # min price of instrument at this time:
1152                prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None
1153
1154                # last price of deal with instrument:
1155                prices["lastPrice"] = NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]) if "lastPrice" in pricesResponse.keys() else 0
1156
1157                # last close price of instrument:
1158                prices["closePrice"] = NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]) if "closePrice" in pricesResponse.keys() else 0
1159
1160            else:
1161                uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi))
1162                uLogger.debug("Server response: {}".format(pricesResponse))
1163
1164            if show:
1165                if prices["buy"] or prices["sell"]:
1166                    info = [
1167                        "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format(
1168                            datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
1169                            self.ticker,
1170                            self.figi,
1171                            self.depth,
1172                        ),
1173                        uLog.sepShort, "\n",
1174                        " Orders of Buyers   | Orders of Sellers\n",
1175                        uLog.sepShort, "\n",
1176                        " Sell prices (vol.) | Buy prices (vol.)\n",
1177                        uLog.sepShort, "\n",
1178                    ]
1179
1180                    if not prices["buy"]:
1181                        info.append("                    | No orders!\n")
1182                        sumBuy = 0
1183
1184                    else:
1185                        sumBuy = sum([x["quantity"] for x in prices["buy"]])
1186                        maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True)
1187                        for item in maxMinSorted:
1188                            info.append("                    | {} ({})\n".format(item["price"], item["quantity"]))
1189
1190                    if not prices["sell"]:
1191                        info.append("No orders!          |\n")
1192                        sumSell = 0
1193
1194                    else:
1195                        sumSell = sum([x["quantity"] for x in prices["sell"]])
1196                        for item in prices["sell"]:
1197                            info.append("{:>19} |\n".format("{} ({})".format(item["price"], item["quantity"])))
1198
1199                    info.extend([
1200                        uLog.sepShort, "\n",
1201                        "{:>19} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)),
1202                        uLog.sepShort, "\n",
1203                    ])
1204
1205                    infoText = "".join(info)
1206
1207                    uLogger.info("Current prices in order book:\n\n{}".format(infoText))
1208
1209                else:
1210                    uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi))
1211
1212        return prices
1213
1214    def ShowInstrumentsInfo(self, show: bool = True) -> str:
1215        """
1216        This method get and show information about all available broker instruments for current user account.
1217        If `instrumentsFile` string is not empty then also save information to this file.
1218
1219        :param show: if `True` then print results to console, if `False` - print only to file.
1220        :return: multi-lines string with all available broker instruments
1221        """
1222        if not self.iList:
1223            self.iList = self.Listing()
1224
1225        info = [
1226            "# All available instruments from Tinkoff Broker server for current user token\n\n",
1227            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1228        ]
1229
1230        # add instruments count by type:
1231        for iType in self.iList.keys():
1232            info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType])))
1233
1234        headerLine = "| Ticker       | Full name                                                 | FIGI         | Cur | Lot     | Step       |\n"
1235        splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n"
1236
1237        # generating info tables with all instruments by type:
1238        for iType in self.iList.keys():
1239            info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine])
1240
1241            for instrument in self.iList[iType].keys():
1242                iName = self.iList[iType][instrument]["name"]  # instrument's name
1243                if len(iName) > 57:
1244                    iName = "{}...".format(iName[:54])  # right trim for a long string
1245
1246                info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format(
1247                    self.iList[iType][instrument]["ticker"],
1248                    iName,
1249                    self.iList[iType][instrument]["figi"],
1250                    self.iList[iType][instrument]["currency"],
1251                    self.iList[iType][instrument]["lot"],
1252                    "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0,
1253                ))
1254
1255        infoText = "".join(info)
1256
1257        if show:
1258            uLogger.info(infoText)
1259
1260        if self.instrumentsFile:
1261            with open(self.instrumentsFile, "w", encoding="UTF-8") as fH:
1262                fH.write(infoText)
1263
1264            uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile)))
1265
1266        return infoText
1267
1268    def SearchInstruments(self, pattern: str, show: bool = True) -> dict:
1269        """
1270        This method search and show information about instruments by part of its ticker, FIGI or name.
1271        If `searchResultsFile` string is not empty then also save information to this file.
1272
1273        :param pattern: string with part of ticker, FIGI or instrument's name.
1274        :param show: if `True` then print results to console, if `False` - return list of result only.
1275        :return: list of dictionaries with all found instruments.
1276        """
1277        if not self.iList:
1278            self.iList = self.Listing()
1279
1280        searchResults = {iType: {} for iType in self.iList}  # same as iList but will contains only filtered instruments
1281        compiledPattern = re.compile(pattern, re.IGNORECASE)
1282
1283        for iType in self.iList:
1284            for instrument in self.iList[iType].values():
1285                searchResult = compiledPattern.search(" ".join(
1286                    [instrument["ticker"], instrument["figi"], instrument["name"]]
1287                ))
1288
1289                if searchResult:
1290                    searchResults[iType][instrument["ticker"]] = instrument
1291
1292        resultsLen = sum([len(searchResults[iType]) for iType in searchResults])
1293        info = [
1294            "# Search results\n\n",
1295            "* **Search pattern:** [{}]\n".format(pattern),
1296            "* **Found instruments:** [{}]\n\n".format(resultsLen),
1297            "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n"
1298        ]
1299        infoShort = info[:]
1300
1301        headerLine = "| Type       | Ticker       | Full name                                                      | FIGI         |\n"
1302        splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n"
1303        skippedLine = "| ...        | ...          | ...                                                            | ...          |\n"
1304
1305        if resultsLen == 0:
1306            info.append("\nNo results\n")
1307            infoShort.append("\nNo results\n")
1308            uLogger.warning("No results. Try changing your search pattern.")
1309
1310        else:
1311            for iType in searchResults:
1312                iTypeValuesCount = len(searchResults[iType].values())
1313                if iTypeValuesCount > 0:
1314                    info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1315                    infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1316
1317                    for instrument in searchResults[iType].values():
1318                        info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format(
1319                            instrument["type"],
1320                            instrument["ticker"],
1321                            "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"],  # right trim for a long string
1322                            instrument["figi"],
1323                        ))
1324
1325                    if iTypeValuesCount <= 5:
1326                        infoShort.extend(info[-iTypeValuesCount:])
1327
1328                    else:
1329                        infoShort.extend(info[-5:])
1330                        infoShort.append(skippedLine)
1331
1332        infoText = "".join(info)
1333        infoTextShort = "".join(infoShort)
1334
1335        if show:
1336            uLogger.info(infoTextShort)
1337            uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`")
1338
1339        if self.searchResultsFile:
1340            with open(self.searchResultsFile, "w", encoding="UTF-8") as fH:
1341                fH.write(infoText)
1342
1343            uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile)))
1344
1345        return searchResults
1346
1347    def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1348        """
1349        Creating list with unique instrument FIGIs from input list of tickers or FIGIs.
1350
1351        :param instruments: list of strings with tickers or FIGIs.
1352        :return: list with unique instrument FIGIs only.
1353        """
1354        requestedInstruments = []
1355        for iName in instruments:
1356            if iName not in self.aliases.keys():
1357                if iName not in requestedInstruments:
1358                    requestedInstruments.append(iName)
1359
1360            else:
1361                if iName not in requestedInstruments:
1362                    if self.aliases[iName] not in requestedInstruments:
1363                        requestedInstruments.append(self.aliases[iName])
1364
1365        uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments))
1366
1367        onlyUniqueFIGIs = []
1368        for iName in requestedInstruments:
1369            if iName in TKS_TICKERS_OR_FIGI_EXCLUDED:
1370                continue
1371
1372            self.ticker = iName
1373            iData = self.SearchByTicker(requestPrice=False)  # trying to find instrument by ticker
1374
1375            if not iData:
1376                self.ticker = ""
1377                self.figi = iName
1378
1379                iData = self.SearchByFIGI(requestPrice=False)  # trying to find instrument by FIGI
1380
1381                if not iData:
1382                    self.figi = ""
1383                    uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName))
1384
1385            if iData and iData["figi"] not in onlyUniqueFIGIs:
1386                onlyUniqueFIGIs.append(iData["figi"])
1387
1388        uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs))
1389
1390        return onlyUniqueFIGIs
1391
1392    def GetListOfPrices(self, instruments: list, show: bool = False) -> list:
1393        """
1394        This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
1395        See limits: https://tinkoff.github.io/investAPI/limits/
1396        If `pricesFile` string is not empty then also save information to this file.
1397
1398        :param instruments: list of strings with tickers or FIGIs.
1399        :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`.
1400        :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1401                 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods.
1402        """
1403        if instruments is None or not instruments:
1404            uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!")
1405            raise Exception("Ticker or FIGI required")
1406
1407        onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments)
1408
1409        uLogger.debug("Requesting current prices from Tinkoff Broker server...")
1410
1411        iList = []  # trying to get info and current prices about all unique instruments:
1412        for self.figi in onlyUniqueFIGIs:
1413            iData = self.SearchByFIGI(requestPrice=True)
1414            iList.append(iData)
1415
1416        self.ShowListOfPrices(iList, show)
1417
1418        return iList
1419
1420    def ShowListOfPrices(self, iList: list, show: bool = True) -> str:
1421        """
1422        Show table contains current prices of given instruments.
1423
1424        :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1425                      One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods.
1426        :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`.
1427        :return: multilines text in Markdown format as a table contains current prices.
1428        """
1429        infoText = ""
1430
1431        if show or self.pricesFile:
1432            info = [
1433                "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1434                "| Ticker       | FIGI         | Type       | Prev. close | Last price  | Chg. %   | Day limits min/max  | Actual sell / buy   | Curr. |\n",
1435                "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n",
1436            ]
1437
1438            for item in iList:
1439                info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format(
1440                    item["ticker"],
1441                    item["figi"],
1442                    item["type"],
1443                    "{:.2f}".format(float(item["currentPrice"]["closePrice"])),
1444                    "{:.2f}".format(float(item["currentPrice"]["lastPrice"])),
1445                    "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])),
1446                    "{} / {}".format(
1447                        item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A",
1448                        item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A",
1449                    ),
1450                    "{} / {}".format(
1451                        item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A",
1452                        item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A",
1453                    ),
1454                    item["currency"],
1455                ))
1456
1457            infoText = "".join(info)
1458
1459            if show:
1460                uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText))
1461
1462            if self.pricesFile:
1463                with open(self.pricesFile, "w", encoding="UTF-8") as fH:
1464                    fH.write(infoText)
1465
1466                uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile)))
1467
1468        return infoText
1469
1470    def RequestTradingStatus(self) -> dict:
1471        """
1472        Requesting trading status for the instrument defined by `figi` variable.
1473        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus
1474        Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
1475
1476        :return: dictionary with trading status attributes. Response example:
1477                 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING",
1478                  "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}`
1479        """
1480        if self.figi is None or not self.figi:
1481            uLogger.error("Variable `figi` must be defined for using this method!")
1482            raise Exception("FIGI required")
1483
1484        uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self.figi))
1485
1486        self.body = str({"figi": self.figi, "instrumentId": self.figi})
1487        tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus"
1488        tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST")
1489
1490        uLogger.debug("Records about current trading status successfully received")
1491
1492        return tradingStatus
1493
1494    def RequestPortfolio(self) -> dict:
1495        """
1496        Requesting actual user's portfolio for current `accountId`.
1497        REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
1498        Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
1499
1500        :return: dictionary with user's portfolio.
1501        """
1502        if self.accountId is None or not self.accountId:
1503            uLogger.error("Variable `accountId` must be defined for using this method!")
1504            raise Exception("Account ID required")
1505
1506        uLogger.debug("Requesting current actual user's portfolio. Wait, please...")
1507
1508        self.body = str({"accountId": self.accountId})
1509        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio"
1510        rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST")
1511
1512        uLogger.debug("Records about user's portfolio successfully received")
1513
1514        return rawPortfolio
1515
1516    def RequestPositions(self) -> dict:
1517        """
1518        Requesting open positions by currencies and instruments for current `accountId`.
1519        REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
1520        Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
1521
1522        :return: dictionary with open positions by instruments.
1523        """
1524        if self.accountId is None or not self.accountId:
1525            uLogger.error("Variable `accountId` must be defined for using this method!")
1526            raise Exception("Account ID required")
1527
1528        uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...")
1529
1530        self.body = str({"accountId": self.accountId})
1531        positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions"
1532        rawPositions = self.SendAPIRequest(positionsURL, reqType="POST")
1533
1534        uLogger.debug("Records about current open positions successfully received")
1535
1536        return rawPositions
1537
1538    def RequestPendingOrders(self) -> list:
1539        """
1540        Requesting current actual pending orders for current `accountId`.
1541        REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
1542        Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
1543
1544        :return: list of dictionaries with pending orders.
1545        """
1546        if self.accountId is None or not self.accountId:
1547            uLogger.error("Variable `accountId` must be defined for using this method!")
1548            raise Exception("Account ID required")
1549
1550        uLogger.debug("Requesting current actual pending orders. Wait, please...")
1551
1552        self.body = str({"accountId": self.accountId})
1553        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders"
1554        rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"]
1555
1556        uLogger.debug("[{}] records about pending orders received".format(len(rawOrders)))
1557
1558        return rawOrders
1559
1560    def RequestStopOrders(self) -> list:
1561        """
1562        Requesting current actual stop orders for current `accountId`.
1563        REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
1564        Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
1565
1566        :return: list of dictionaries with stop orders.
1567        """
1568        if self.accountId is None or not self.accountId:
1569            uLogger.error("Variable `accountId` must be defined for using this method!")
1570            raise Exception("Account ID required")
1571
1572        uLogger.debug("Requesting current actual stop orders. Wait, please...")
1573
1574        self.body = str({"accountId": self.accountId})
1575        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders"
1576        rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"]
1577
1578        uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders)))
1579
1580        return rawStopOrders
1581
1582    def Overview(self, show: bool = False, details: str = "full") -> dict:
1583        """
1584        Get portfolio: all open positions, orders and some statistics for current `accountId`.
1585        If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile`
1586        are defined then also save information to file.
1587
1588        WARNING! It is not recommended to run this method too many times in a loop! The server receives
1589        many requests about the state of the portfolio, and then, based on the received data, a large number
1590        of calculation and statistics are collected.
1591
1592        :param show: if `False` then only dictionary returns, if `True` then show more debug information.
1593        :param details: how detailed should the information be? You should specify one of strings:
1594                        `full` - shows full available information about portfolio status (by default),
1595                        `positions` - shows only open positions,
1596                        `digest` - show a short digest of the portfolio status,
1597                        `analytics` - shows only the analytics section and the distribution of the portfolio by various categories,
1598                        `orders` - shows only sections of open limits and stop orders.
1599        :return: dictionary with client's raw portfolio and some statistics.
1600        """
1601        if self.accountId is None or not self.accountId:
1602            uLogger.error("Variable `accountId` must be defined for using this method!")
1603            raise Exception("Account ID required")
1604
1605        view = {
1606            "raw": {  # --- raw portfolio responses from broker with user portfolio data:
1607                "headers": {},  # list of dictionaries, response headers without "positions" section
1608                "Currencies": [],  # list of dictionaries, open trades with currencies from "positions" section
1609                "Shares": [],  # list of dictionaries, open trades with shares from "positions" section
1610                "Bonds": [],  # list of dictionaries, open trades with bonds from "positions" section
1611                "Etfs": [],  # list of dictionaries, open trades with etfs from "positions" section
1612                "Futures": [],  # list of dictionaries, open trades with futures from "positions" section
1613                "positions": {},  # raw response from broker: dictionary with current available or blocked currencies and instruments for client
1614                "orders": [],  # raw response from broker: list of dictionaries with all pending (market) orders
1615                "stopOrders": [],  # raw response from broker: list of dictionaries with all stop orders
1616                "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}},  # dict with prices of all currencies in RUB
1617            },
1618            "stat": {  # --- some statistics calculated using "raw" sections:
1619                "portfolioCostRUB": 0.,  # portfolio cost in RUB (Russian Rouble)
1620                "availableRUB": 0.,  # available rubles (without other currencies)
1621                "blockedRUB": 0.,  # blocked sum in Russian Rouble
1622                "totalChangesRUB": 0.,  # changes for all open trades in RUB
1623                "totalChangesPercentRUB": 0.,  # changes for all open trades in percents
1624                "allCurrenciesCostRUB": 0.,  # costs of all currencies (include rubles) in RUB
1625                "sharesCostRUB": 0.,  # costs of all shares in RUB
1626                "bondsCostRUB": 0.,  # costs of all bonds in RUB
1627                "etfsCostRUB": 0.,  # costs of all etfs in RUB
1628                "futuresCostRUB": 0.,  # costs of all futures in RUB
1629                "Currencies": [],  # list of dictionaries of all currencies statistics
1630                "Shares": [],  # list of dictionaries of all shares statistics
1631                "Bonds": [],  # list of dictionaries of all bonds statistics
1632                "Etfs": [],  # list of dictionaries of all etfs statistics
1633                "Futures": [],  # list of dictionaries of all futures statistics
1634                "orders": [],  # list of dictionaries of all pending (market) orders and it's parameters
1635                "stopOrders": [],  # list of dictionaries of all stop orders and it's parameters
1636                "blockedCurrencies": {},  # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21}
1637                "blockedInstruments": {},  # dict with blocked  by FIGI, e.g. {}
1638                "funds": {},  # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1639            },
1640            "analytics": {  # --- some analytics of portfolio:
1641                "distrByAssets": {},  # portfolio distribution by assets
1642                "distrByCompanies": {},  # portfolio distribution by companies
1643                "distrBySectors": {},  # portfolio distribution by sectors
1644                "distrByCurrencies": {},  # portfolio distribution by currencies
1645                "distrByCountries": {},  # portfolio distribution by countries
1646            }
1647        }
1648
1649        details = details.lower()
1650        availableDetails = ["full", "positions", "digest", "analytics", "orders"]
1651        if details not in availableDetails:
1652            details = "full"
1653            uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails))
1654
1655        uLogger.debug("Requesting portfolio of a client. Wait, please...")
1656
1657        portfolioResponse = self.RequestPortfolio()  # current user's portfolio (dict)
1658        view["raw"]["positions"] = self.RequestPositions()  # current open positions by instruments (dict)
1659        view["raw"]["orders"] = self.RequestPendingOrders()  # current actual pending orders (list)
1660        view["raw"]["stopOrders"] = self.RequestStopOrders()  # current actual stop orders (list)
1661
1662        # save response headers without "positions" section:
1663        for key in portfolioResponse.keys():
1664            if key != "positions":
1665                view["raw"]["headers"][key] = portfolioResponse[key]
1666
1667            else:
1668                continue
1669
1670        # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation
1671        # Type of instrument must be only one of supported types in TKS_INSTRUMENTS
1672        for item in portfolioResponse["positions"]:
1673            if item["instrumentType"] == "currency":
1674                self.figi = item["figi"]
1675                curr = self.SearchByFIGI(requestPrice=False)
1676
1677                # current price of currency in RUB:
1678                view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = {
1679                    "name": curr["name"],
1680                    "currentPrice": NanoToFloat(
1681                        item["currentPrice"]["units"],
1682                        item["currentPrice"]["nano"]
1683                    ),
1684                }
1685
1686                view["raw"]["Currencies"].append(item)
1687
1688            elif item["instrumentType"] == "share":
1689                view["raw"]["Shares"].append(item)
1690
1691            elif item["instrumentType"] == "bond":
1692                view["raw"]["Bonds"].append(item)
1693
1694            elif item["instrumentType"] == "etf":
1695                view["raw"]["Etfs"].append(item)
1696
1697            elif item["instrumentType"] == "futures":
1698                view["raw"]["Futures"].append(item)
1699
1700            else:
1701                continue
1702
1703        # how many volume of currencies (by ISO currency name) are blocked:
1704        for item in view["raw"]["positions"]["blocked"]:
1705            blocked = NanoToFloat(item["units"], item["nano"])
1706            if blocked > 0:
1707                view["stat"]["blockedCurrencies"][item["currency"]] = blocked
1708
1709        # how many volume of instruments (by FIGI) are blocked:
1710        for item in view["raw"]["positions"]["securities"]:
1711            blocked = int(item["blocked"])
1712            if blocked > 0:
1713                view["stat"]["blockedInstruments"][item["figi"]] = blocked
1714
1715        allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]}
1716
1717        if "rub" in allBlocked.keys():
1718            view["stat"]["blockedRUB"] = allBlocked["rub"]  # blocked rubles
1719
1720        # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies:
1721        view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"])
1722        view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"])
1723        view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"])
1724        view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"])
1725        view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"])
1726        view["stat"]["portfolioCostRUB"] = sum([
1727            view["stat"]["allCurrenciesCostRUB"],
1728            view["stat"]["sharesCostRUB"],
1729            view["stat"]["bondsCostRUB"],
1730            view["stat"]["etfsCostRUB"],
1731            view["stat"]["futuresCostRUB"],
1732        ])
1733
1734        # --- calculating some portfolio statistics:
1735        byComp = {}  # distribution by companies
1736        bySect = {}  # distribution by sectors
1737        byCurr = {}  # distribution by currencies (include RUB)
1738        unknownCountryName = "All other countries"  # default name for instruments without "countryOfRisk" and "countryOfRiskName"
1739        byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}}  # distribution by countries (currencies are included in their countries)
1740
1741        for item in portfolioResponse["positions"]:
1742            self.figi = item["figi"]
1743            instrument = self.SearchByFIGI(requestPrice=False)  # full raw info about instrument by FIGI
1744
1745            if instrument:
1746                if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys():
1747                    blocked = allBlocked[instrument["nominal"]["currency"]]  # blocked volume of currency
1748
1749                elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys():
1750                    blocked = allBlocked[item["figi"]]  # blocked volume of other instruments
1751
1752                else:
1753                    blocked = 0
1754
1755                volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"])  # available volume of instrument
1756                lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"])  # available volume in lots of instrument
1757                direction = "Long" if lots >= 0 else "Short"  # direction of an instrument's position: short or long
1758                curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"])  # current instrument's price
1759                average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"])  # current average position price
1760                profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"])  # expected profit at current moment
1761                currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"]  # currency name rub, usd, eur etc.
1762                cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume  # current cost of all volume of instrument in basic asset
1763                baseCurrencyName = item["currentPrice"]["currency"]  # name of base currency (rub)
1764                countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName
1765                costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"]  # cost in rubles
1766                percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.  # instrument's part in percent of full portfolio cost
1767
1768                statData = {
1769                    "figi": item["figi"],  # FIGI from REST API "GetPortfolio" method
1770                    "ticker": instrument["ticker"],  # ticker by FIGI
1771                    "currency": currency,  # currency name rub, usd, eur etc. for instrument price
1772                    "volume": volume,  # available volume of instrument
1773                    "lots": lots,  # volume in lots of instrument
1774                    "direction": direction,  # direction of an instrument's position: short or long
1775                    "blocked": blocked,  # blocked volume of currency or instrument
1776                    "currentPrice": curPrice,  # current instrument's price in basic asset
1777                    "average": average,  # current average position price
1778                    "cost": cost,  # current cost of all volume of instrument in basic asset
1779                    "baseCurrencyName": baseCurrencyName,  # name of base currency (rub)
1780                    "costRUB": costRUB,  # cost of instrument in ruble
1781                    "percentCostRUB": percentCostRUB,  # instrument's part in percent of full portfolio cost in RUB
1782                    "profit": profit,  # expected profit at current moment
1783                    "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0,  # expected percents of profit at current moment for this instrument
1784                    "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other",
1785                    "name": instrument["name"] if "name" in instrument.keys() else "",  # human-readable names of instruments
1786                    "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "",  # ISO name for currencies only
1787                    "country": countryName,  # e.g. "[RU] Российская Федерация" or unknownCountryName
1788                    "step": instrument["step"],  # minimum price increment
1789                }
1790
1791                # adding distribution by unique countries:
1792                if statData["country"] not in byCountry.keys():
1793                    byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB}
1794
1795                else:
1796                    byCountry[statData["country"]]["cost"] += costRUB
1797                    byCountry[statData["country"]]["percent"] += percentCostRUB
1798
1799                if item["instrumentType"] != "currency":
1800                    # adding distribution by unique companies:
1801                    if statData["name"]:
1802                        if statData["name"] not in byComp.keys():
1803                            byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB}
1804
1805                        else:
1806                            byComp[statData["name"]]["cost"] += costRUB
1807                            byComp[statData["name"]]["percent"] += percentCostRUB
1808
1809                    # adding distribution by unique sectors:
1810                    if statData["sector"] not in bySect.keys():
1811                        bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB}
1812
1813                    else:
1814                        bySect[statData["sector"]]["cost"] += costRUB
1815                        bySect[statData["sector"]]["percent"] += percentCostRUB
1816
1817                # adding distribution by unique currencies:
1818                if currency not in byCurr.keys():
1819                    byCurr[currency] = {
1820                        "name": view["raw"]["currenciesCurrentPrices"][currency]["name"],
1821                        "cost": costRUB,
1822                        "percent": percentCostRUB
1823                    }
1824
1825                else:
1826                    byCurr[currency]["cost"] += costRUB
1827                    byCurr[currency]["percent"] += percentCostRUB
1828
1829                # saving statistics for every instrument:
1830                if item["instrumentType"] == "currency":
1831                    view["stat"]["Currencies"].append(statData)
1832
1833                    # update dict with free funds for trading (total - blocked) by currencies
1834                    # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1835                    view["stat"]["funds"][currency] = {
1836                        "total": volume,
1837                        "totalCostRUB": costRUB,  # total volume cost in rubles
1838                        "free": volume - blocked,
1839                        "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0,  # free volume cost in rubles
1840                    }
1841
1842                elif item["instrumentType"] == "share":
1843                    view["stat"]["Shares"].append(statData)
1844
1845                elif item["instrumentType"] == "bond":
1846                    view["stat"]["Bonds"].append(statData)
1847
1848                elif item["instrumentType"] == "etf":
1849                    view["stat"]["Etfs"].append(statData)
1850
1851                elif item["instrumentType"] == "Futures":
1852                    view["stat"]["Futures"].append(statData)
1853
1854                else:
1855                    continue
1856
1857        # total changes in Russian Ruble:
1858        view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]])  # available RUB without other currencies
1859        view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0.
1860        startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100)
1861        view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost
1862        view["stat"]["funds"]["rub"] = {
1863            "total": view["stat"]["availableRUB"],
1864            "totalCostRUB": view["stat"]["availableRUB"],
1865            "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1866            "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1867        }
1868
1869        # --- pending orders sector data:
1870        uniquePendingOrders = []
1871        uniquePendingOrdersFIGIs = []
1872        for item in view["raw"]["orders"]:
1873            if item["figi"] not in uniquePendingOrdersFIGIs:
1874                uniquePendingOrdersFIGIs.append(item["figi"])
1875                uniquePendingOrders.append(item)
1876
1877        for item in uniquePendingOrders:
1878            self.figi = item["figi"]
1879            instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI
1880
1881            if instrument:
1882                action = TKS_ORDER_DIRECTIONS[item["direction"]]
1883                orderType = TKS_ORDER_TYPES[item["orderType"]]
1884                orderState = TKS_ORDER_STATES[item["executionReportStatus"]]
1885                orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1886
1887                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1888                if item["direction"] == "ORDER_DIRECTION_BUY":
1889                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1890
1891                else:
1892                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1893
1894                # requested price for order execution:
1895                target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"])
1896
1897                # necessary changes in percent to reach target from current price:
1898                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1899
1900                view["stat"]["orders"].append({
1901                    "orderID": item["orderId"],  # orderId number parameter of current order
1902                    "figi": item["figi"],  # FIGI identification
1903                    "ticker": instrument["ticker"],  # ticker name by FIGI
1904                    "lotsRequested": item["lotsRequested"],  # requested lots value
1905                    "lotsExecuted": item["lotsExecuted"],  # how many lots are executed
1906                    "currentPrice": lastPrice,  # current instrument's price for defined action
1907                    "targetPrice": target,  # requested price for order execution in base currency
1908                    "baseCurrencyName": item["initialSecurityPrice"]["currency"],  # name of base currency
1909                    "percentChanges": changes,  # changes in percent to target from current price
1910                    "currency": item["currency"],  # instrument's currency name
1911                    "action": action,  # sell / buy / Unknown from TKS_ORDER_DIRECTIONS
1912                    "type": orderType,  # type of order from TKS_ORDER_TYPES
1913                    "status": orderState,  # order status from TKS_ORDER_STATES
1914                    "date": orderDate,  # string with order date and time from UTC format (without nano seconds part)
1915                })
1916
1917        # --- stop orders sector data:
1918        uniqueStopOrders = []
1919        uniqueStopOrdersFIGIs = []
1920        for item in view["raw"]["stopOrders"]:
1921            if item["figi"] not in uniqueStopOrdersFIGIs:
1922                uniqueStopOrdersFIGIs.append(item["figi"])
1923                uniqueStopOrders.append(item)
1924
1925        for item in uniqueStopOrders:
1926            self.figi = item["figi"]
1927            instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI
1928
1929            if instrument:
1930                action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]]
1931                orderType = TKS_STOP_ORDER_TYPES[item["orderType"]]
1932                createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1933
1934                # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order
1935                if "expirationTime" in item.keys():
1936                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"]
1937                    expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0]
1938
1939                else:
1940                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"]
1941                    expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"]
1942
1943                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1944                if item["direction"] == "STOP_ORDER_DIRECTION_BUY":
1945                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1946
1947                else:
1948                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1949
1950                # requested price when stop-order executed:
1951                target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"])
1952
1953                # price for limit-order, set up when stop-order executed:
1954                limit = NanoToFloat(item["price"]["units"], item["price"]["nano"])
1955
1956                # necessary changes in percent to reach target from current price:
1957                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1958
1959                view["stat"]["stopOrders"].append({
1960                    "orderID": item["stopOrderId"],  # stopOrderId number parameter of current stop-order
1961                    "figi": item["figi"],  # FIGI identification
1962                    "ticker": instrument["ticker"],  # ticker name by FIGI
1963                    "lotsRequested": item["lotsRequested"],  # requested lots value
1964                    "currentPrice": lastPrice,  # current instrument's price for defined action
1965                    "targetPrice": target,  # requested price for stop-order execution in base currency
1966                    "limitPrice": limit,  # price for limit-order, set up when stop-order executed, 0 if market order
1967                    "baseCurrencyName": item["stopPrice"]["currency"],  # name of base currency
1968                    "percentChanges": changes,  # changes in percent to target from current price
1969                    "currency": item["currency"],  # instrument's currency name
1970                    "action": action,  # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS
1971                    "type": orderType,  # type of order from TKS_STOP_ORDER_TYPES
1972                    "expType": expType,  # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES
1973                    "createDate": createDate,  # string with created order date and time from UTC format (without nano seconds part)
1974                    "expDate": expDate,  # string with expiration order date and time from UTC format (without nano seconds part)
1975                })
1976
1977        # --- calculating data for analytics section:
1978        # portfolio distribution by assets:
1979        view["analytics"]["distrByAssets"] = {
1980            "Ruble": {
1981                "uniques": 1,
1982                "cost": view["stat"]["availableRUB"],
1983                "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1984            },
1985            "Currencies": {
1986                "uniques": len(view["stat"]["Currencies"]),  # all foreign currencies without RUB
1987                "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"],
1988                "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1989            },
1990            "Shares": {
1991                "uniques": len(view["stat"]["Shares"]),
1992                "cost": view["stat"]["sharesCostRUB"],
1993                "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1994            },
1995            "Bonds": {
1996                "uniques": len(view["stat"]["Bonds"]),
1997                "cost": view["stat"]["bondsCostRUB"],
1998                "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1999            },
2000            "Etfs": {
2001                "uniques": len(view["stat"]["Etfs"]),
2002                "cost": view["stat"]["etfsCostRUB"],
2003                "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2004            },
2005            "Futures": {
2006                "uniques": len(view["stat"]["Futures"]),
2007                "cost": view["stat"]["futuresCostRUB"],
2008                "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2009            },
2010        }
2011
2012        # portfolio distribution by companies:
2013        view["analytics"]["distrByCompanies"]["All money cash"] = {
2014            "ticker": "",
2015            "cost": view["stat"]["allCurrenciesCostRUB"],
2016            "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2017        }
2018        view["analytics"]["distrByCompanies"].update(byComp)
2019
2020        # portfolio distribution by sectors:
2021        view["analytics"]["distrBySectors"]["All money cash"] = {
2022            "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"],
2023            "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"],
2024        }
2025        view["analytics"]["distrBySectors"].update(bySect)
2026
2027        # portfolio distribution by currencies:
2028        if "rub" not in view["analytics"]["distrByCurrencies"].keys():
2029            uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!")
2030            view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0}
2031
2032        view["analytics"]["distrByCurrencies"].update(byCurr)
2033        view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2034        view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2035
2036        # portfolio distribution by countries:
2037        if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys():
2038            uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!")
2039            view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0}
2040
2041        view["analytics"]["distrByCountries"].update(byCountry)
2042        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2043        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2044
2045        # --- Prepare text statistics overview in human-readable:
2046        if show:
2047            # Whatever the value `details`, header not changes:
2048            info = [
2049                "# Client's portfolio\n\n",
2050                "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
2051                "* **Account ID:** [{}]\n".format(self.accountId),
2052            ]
2053
2054            if details in ["full", "positions", "digest"]:
2055                info.extend([
2056                    "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2057                    "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format(
2058                        "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2059                        view["stat"]["totalChangesRUB"],
2060                        "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2061                        view["stat"]["totalChangesPercentRUB"],
2062                    ),
2063                ])
2064
2065            if details in ["full", "positions"]:
2066                info.extend([
2067                    "## Open positions\n\n",
2068                    "| Ticker [FIGI]               | Volume (blocked)                | Lots     | Curr. price  | Avg. price   | Current volume cost | Profit (%)                   |\n",
2069                    "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n",
2070                    "| Ruble                       | {:>31} |          |              |              |                     |                              |\n".format(
2071                        "{:.2f} ({:.2f}) rub".format(
2072                            view["stat"]["availableRUB"],
2073                            view["stat"]["blockedRUB"],
2074                        )
2075                    )
2076                ])
2077
2078                def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list:
2079                    return [
2080                        "|                             |                                 |          |              |              |                     |                              |\n",
2081                        "| {:<27} |                                 |          |              |              | {:>19} |                              |\n".format(
2082                            noTradeStr if noTradeStr else typeStr,
2083                            "" if noTradeStr else "{:.2f} RUB".format(CostRUB),
2084                        ),
2085                    ]
2086
2087                def _InfoStr(data: dict, showCurrencyName: bool = False) -> str:
2088                    return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format(
2089                        "{} [{}]".format(data["ticker"], data["figi"]),
2090                        "{:.2f} ({:.2f}) {}".format(
2091                            data["volume"],
2092                            data["blocked"],
2093                            data["currency"],
2094                        ) if showCurrencyName else "{:.0f} ({:.0f})".format(
2095                            data["volume"],
2096                            data["blocked"],
2097                        ),
2098                        "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]),
2099                        "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a",
2100                        "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a",
2101                        "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]),
2102                        "{}{:.2f} {} ({}{:.2f}%)".format(
2103                            "+" if data["profit"] > 0 else "",
2104                            data["profit"], data["baseCurrencyName"],
2105                            "+" if data["percentProfit"] > 0 else "",
2106                            data["percentProfit"],
2107                        ),
2108                    )
2109
2110                # --- Show currencies section:
2111                if view["stat"]["Currencies"]:
2112                    info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**"))
2113                    for item in view["stat"]["Currencies"]:
2114                        info.append(_InfoStr(item, showCurrencyName=True))
2115
2116                else:
2117                    info.extend(_SplitStr(noTradeStr="**Currencies:** no trades"))
2118
2119                # --- Show shares section:
2120                if view["stat"]["Shares"]:
2121                    info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**"))
2122
2123                    for item in view["stat"]["Shares"]:
2124                        info.append(_InfoStr(item))
2125
2126                else:
2127                    info.extend(_SplitStr(noTradeStr="**Shares:** no trades"))
2128
2129                # --- Show bonds section:
2130                if view["stat"]["Bonds"]:
2131                    info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**"))
2132
2133                    for item in view["stat"]["Bonds"]:
2134                        info.append(_InfoStr(item))
2135
2136                else:
2137                    info.extend(_SplitStr(noTradeStr="**Bonds:** no trades"))
2138
2139                # --- Show etfs section:
2140                if view["stat"]["Etfs"]:
2141                    info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**"))
2142
2143                    for item in view["stat"]["Etfs"]:
2144                        info.append(_InfoStr(item))
2145
2146                else:
2147                    info.extend(_SplitStr(noTradeStr="**Etfs:** no trades"))
2148
2149                # --- Show futures section:
2150                if view["stat"]["Futures"]:
2151                    info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**"))
2152
2153                    for item in view["stat"]["Futures"]:
2154                        info.append(_InfoStr(item))
2155
2156                else:
2157                    info.extend(_SplitStr(noTradeStr="**Futures:** no trades"))
2158
2159            if details in ["full", "orders"]:
2160                # --- Show pending orders section:
2161                if view["stat"]["orders"]:
2162                    info.extend([
2163                        "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])),
2164                        "\n| Ticker [FIGI]               | Order ID       | Lots (exec.) | Current price (% delta) | Target price  | Action    | Type      | Create date (UTC)       |\n",
2165                        "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n",
2166                    ])
2167
2168                    for item in view["stat"]["orders"]:
2169                        info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format(
2170                            "{} [{}]".format(item["ticker"], item["figi"]),
2171                            item["orderID"],
2172                            "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]),
2173                            "{} {} ({}{:.2f}%)".format(
2174                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2175                                item["baseCurrencyName"],
2176                                "+" if item["percentChanges"] > 0 else "",
2177                                float(item["percentChanges"]),
2178                            ),
2179                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2180                            item["action"],
2181                            item["type"],
2182                            item["date"],
2183                        ))
2184
2185                else:
2186                    info.append("\n## Total pending limit-orders: 0\n")
2187
2188                # --- Show stop orders section:
2189                if view["stat"]["stopOrders"]:
2190                    info.extend([
2191                        "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])),
2192                        "\n| Ticker [FIGI]               | Stop order ID                        | Lots   | Current price (% delta) | Target price  | Limit price   | Action    | Type        | Expire type  | Create date (UTC)   | Expiration (UTC)    |\n",
2193                        "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n",
2194                    ])
2195
2196                    for item in view["stat"]["stopOrders"]:
2197                        info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format(
2198                            "{} [{}]".format(item["ticker"], item["figi"]),
2199                            item["orderID"],
2200                            item["lotsRequested"],
2201                            "{} {} ({}{:.2f}%)".format(
2202                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2203                                item["baseCurrencyName"],
2204                                "+" if item["percentChanges"] > 0 else "",
2205                                float(item["percentChanges"]),
2206                            ),
2207                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2208                            "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"],
2209                            item["action"],
2210                            item["type"],
2211                            item["expType"],
2212                            item["createDate"],
2213                            item["expDate"],
2214                        ))
2215
2216                else:
2217                    info.append("\n## Total stop-orders: 0\n")
2218
2219            if details in ["full", "analytics"]:
2220                # -- Show analytics section:
2221                if view["stat"]["portfolioCostRUB"] > 0:
2222                    info.extend([
2223                        "\n# Analytics\n"
2224                        "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2225                        "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format(
2226                            "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2227                            view["stat"]["totalChangesRUB"],
2228                            "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2229                            view["stat"]["totalChangesPercentRUB"],
2230                        ),
2231                        "\n## Portfolio distribution by assets\n"
2232                        "\n| Type       | Uniques | Percent | Current cost       |\n",
2233                        "|------------|---------|---------|--------------------|\n",
2234                    ])
2235
2236                    for key in view["analytics"]["distrByAssets"].keys():
2237                        if view["analytics"]["distrByAssets"][key]["cost"] > 0:
2238                            info.append("| {:<10} | {:<7} | {:<7} | {:<18} |\n".format(
2239                                key,
2240                                view["analytics"]["distrByAssets"][key]["uniques"],
2241                                "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]),
2242                                "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]),
2243                            ))
2244
2245                    maxLenNames = 3 + max([len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"]) for company in view["analytics"]["distrByCompanies"].keys()])
2246                    info.extend([
2247                        "\n## Portfolio distribution by companies\n"
2248                        "\n| Company{} | Percent | Current cost       |\n".format(" " * (maxLenNames - 7)),
2249                        "|--------{}-|---------|--------------------|\n".format("-" * (maxLenNames - 7)),
2250                    ])
2251
2252                    for company in view["analytics"]["distrByCompanies"].keys():
2253                        if view["analytics"]["distrByCompanies"][company]["cost"] > 0:
2254                            nameLen = len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"])
2255                            info.append("| {} | {:<7} | {:<18} |\n".format(
2256                                "{}{}{}".format(
2257                                    "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "",
2258                                    company,
2259                                    "" if nameLen == maxLenNames else "{}".format(" " * (maxLenNames - nameLen - 3) if view["analytics"]["distrByCompanies"][company]["ticker"] else " " * (maxLenNames - nameLen)),
2260                                ),
2261                                "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]),
2262                                "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]),
2263                            ))
2264
2265                    maxLenSectors = max([len(sector) for sector in view["analytics"]["distrBySectors"].keys()])
2266                    info.extend([
2267                        "\n## Portfolio distribution by sectors\n"
2268                        "\n| Sector{} | Percent | Current cost       |\n".format(" " * (maxLenSectors - 6)),
2269                        "|-------{}-|---------|--------------------|\n".format("-" * (maxLenSectors - 6)),
2270                    ])
2271
2272                    for sector in view["analytics"]["distrBySectors"].keys():
2273                        if view["analytics"]["distrBySectors"][sector]["cost"] > 0:
2274                            info.append("| {}{} | {:<7} | {:<18} |\n".format(
2275                                sector,
2276                                "" if len(sector) == maxLenSectors else " " * (maxLenSectors - len(sector)),
2277                                "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]),
2278                                "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]),
2279                            ))
2280
2281                    maxLenMoney = 3 + max([len(currency) + len(view["analytics"]["distrByCurrencies"][currency]["name"]) for currency in view["analytics"]["distrByCurrencies"].keys()])
2282                    info.extend([
2283                        "\n## Portfolio distribution by currencies\n"
2284                        "\n| Instruments currencies{} | Percent | Current cost       |\n".format(" " * (maxLenMoney - 22)),
2285                        "|-----------------------{}-|---------|--------------------|\n".format("-" * (maxLenMoney - 22)),
2286                    ])
2287
2288                    for curr in view["analytics"]["distrByCurrencies"].keys():
2289                        if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0:
2290                            nameLen = 3 + len(curr) + len(view["analytics"]["distrByCurrencies"][curr]["name"])
2291                            info.append("| {} | {:<7} | {:<18} |\n".format(
2292                                "[{}] {}{}".format(
2293                                    curr,
2294                                    view["analytics"]["distrByCurrencies"][curr]["name"],
2295                                    "" if nameLen == maxLenMoney else " " * (maxLenMoney - nameLen),
2296                                ),
2297                                "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]),
2298                                "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]),
2299                            ))
2300
2301                    maxLenCountry = max(17, max([len(country) for country in view["analytics"]["distrByCountries"].keys()]))
2302                    info.extend([
2303                        "\n## Portfolio distribution by countries\n"
2304                        "\n| Assets by country{} | Percent | Current cost       |\n".format(" " * (maxLenCountry - 17)),
2305                        "|------------------{}-|---------|--------------------|\n".format("-" * (maxLenCountry - 17)),
2306                    ])
2307
2308                    for country in view["analytics"]["distrByCountries"].keys():
2309                        if view["analytics"]["distrByCountries"][country]["cost"] > 0:
2310                            nameLen = len(country)
2311                            info.append("| {} | {:<7} | {:<18} |\n".format(
2312                                "{}{}".format(
2313                                    country,
2314                                    "" if nameLen == maxLenCountry else " " * (maxLenCountry - nameLen),
2315                                ),
2316                                "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]),
2317                                "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]),
2318                            ))
2319
2320            infoText = "".join(info)
2321
2322            uLogger.info(infoText)
2323
2324            if details == "full" and self.overviewFile:
2325                filename = self.overviewFile
2326
2327            elif details == "digest" and self.overviewDigestFile:
2328                filename = self.overviewDigestFile
2329
2330            elif details == "positions" and self.overviewPositionsFile:
2331                filename = self.overviewPositionsFile
2332
2333            elif details == "orders" and self.overviewOrdersFile:
2334                filename = self.overviewOrdersFile
2335
2336            elif details == "analytics" and self.overviewAnalyticsFile:
2337                filename = self.overviewAnalyticsFile
2338
2339            else:
2340                filename = ""
2341
2342            if filename:
2343                with open(filename, "w", encoding="UTF-8") as fH:
2344                    fH.write(infoText)
2345
2346                uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename)))
2347
2348        return view
2349
2350    def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple:
2351        """
2352        Returns history operations between two given dates for current `accountId`.
2353        If `reportFile` string is not empty then also save human-readable report.
2354        Shows some statistical data of closed positions.
2355
2356        :param start: see docstring in `GetDatesAsString()` method
2357        :param end: see docstring in `GetDatesAsString()` method
2358        :param show: if `True` then also prints all records to the console.
2359        :param showCancelled: if `False` then remove information about cancelled operations from the deals report.
2360        :return: original list of dictionaries with history of deals records from API ("operations" key):
2361                 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2362                 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2363        """
2364        if self.accountId is None or not self.accountId:
2365            uLogger.error("Variable `accountId` must be defined for using this method!")
2366            raise Exception("Account ID required")
2367
2368        startDate, endDate = GetDatesAsString(start, end)  # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2369
2370        uLogger.debug("Requesting history of a client's operations. Wait, please...")
2371
2372        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2373        dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations"
2374        self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate})
2375        ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"]  # list of dict: operations returns by broker
2376        customStat = {}  # custom statistics in additional to responseJSON
2377
2378        # --- output report in human-readable format:
2379        if show or self.reportFile:
2380            splitLine1 = "|                            |                               |                              |                      |                        |\n"  # Summary section
2381            splitLine2 = "|                     |              |              |            |           |                 |            |                                                                    |\n"  # Operations section
2382            nextDay = ""
2383
2384            info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])]
2385
2386            if len(ops) > 0:
2387                customStat = {
2388                    "opsCount": 0,  # total operations count
2389                    "buyCount": 0,  # buy operations
2390                    "sellCount": 0,  # sell operations
2391                    "buyTotal": {"rub": 0.},  # Buy sums in different currencies
2392                    "sellTotal": {"rub": 0.},  # Sell sums in different currencies
2393                    "payIn": {"rub": 0.},  # Deposit brokerage account
2394                    "payOut": {"rub": 0.},  # Withdrawals
2395                    "divs": {"rub": 0.},  # Dividends income
2396                    "coupons": {"rub": 0.},  # Coupon's income
2397                    "brokerCom": {"rub": 0.},  # Service commissions
2398                    "serviceCom": {"rub": 0.},  # Service commissions
2399                    "marginCom": {"rub": 0.},  # Margin commissions
2400                    "allTaxes": {"rub": 0.},  # Sum of withholding taxes and corrections
2401                }
2402
2403                # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES:
2404                for item in ops:
2405                    if item["state"] == "OPERATION_STATE_EXECUTED":
2406                        payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2407
2408                        # count buy operations:
2409                        if "_BUY" in item["operationType"]:
2410                            customStat["buyCount"] += 1
2411
2412                            if item["payment"]["currency"] in customStat["buyTotal"].keys():
2413                                customStat["buyTotal"][item["payment"]["currency"]] += payment
2414
2415                            else:
2416                                customStat["buyTotal"][item["payment"]["currency"]] = payment
2417
2418                        # count sell operations:
2419                        elif "_SELL" in item["operationType"]:
2420                            customStat["sellCount"] += 1
2421
2422                            if item["payment"]["currency"] in customStat["sellTotal"].keys():
2423                                customStat["sellTotal"][item["payment"]["currency"]] += payment
2424
2425                            else:
2426                                customStat["sellTotal"][item["payment"]["currency"]] = payment
2427
2428                        # count incoming operations:
2429                        elif item["operationType"] in ["OPERATION_TYPE_INPUT"]:
2430                            if item["payment"]["currency"] in customStat["payIn"].keys():
2431                                customStat["payIn"][item["payment"]["currency"]] += payment
2432
2433                            else:
2434                                customStat["payIn"][item["payment"]["currency"]] = payment
2435
2436                        # count withdrawals operations:
2437                        elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]:
2438                            if item["payment"]["currency"] in customStat["payOut"].keys():
2439                                customStat["payOut"][item["payment"]["currency"]] += payment
2440
2441                            else:
2442                                customStat["payOut"][item["payment"]["currency"]] = payment
2443
2444                        # count dividends income:
2445                        elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]:
2446                            if item["payment"]["currency"] in customStat["divs"].keys():
2447                                customStat["divs"][item["payment"]["currency"]] += payment
2448
2449                            else:
2450                                customStat["divs"][item["payment"]["currency"]] = payment
2451
2452                        # count coupon's income:
2453                        elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]:
2454                            if item["payment"]["currency"] in customStat["coupons"].keys():
2455                                customStat["coupons"][item["payment"]["currency"]] += payment
2456
2457                            else:
2458                                customStat["coupons"][item["payment"]["currency"]] = payment
2459
2460                        # count broker commissions:
2461                        elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]:
2462                            if item["payment"]["currency"] in customStat["brokerCom"].keys():
2463                                customStat["brokerCom"][item["payment"]["currency"]] += payment
2464
2465                            else:
2466                                customStat["brokerCom"][item["payment"]["currency"]] = payment
2467
2468                        # count service commissions:
2469                        elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]:
2470                            if item["payment"]["currency"] in customStat["serviceCom"].keys():
2471                                customStat["serviceCom"][item["payment"]["currency"]] += payment
2472
2473                            else:
2474                                customStat["serviceCom"][item["payment"]["currency"]] = payment
2475
2476                        # count margin commissions:
2477                        elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]:
2478                            if item["payment"]["currency"] in customStat["marginCom"].keys():
2479                                customStat["marginCom"][item["payment"]["currency"]] += payment
2480
2481                            else:
2482                                customStat["marginCom"][item["payment"]["currency"]] = payment
2483
2484                        # count withholding taxes:
2485                        elif "_TAX" in item["operationType"]:
2486                            if item["payment"]["currency"] in customStat["allTaxes"].keys():
2487                                customStat["allTaxes"][item["payment"]["currency"]] += payment
2488
2489                            else:
2490                                customStat["allTaxes"][item["payment"]["currency"]] = payment
2491
2492                        else:
2493                            continue
2494
2495                customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"]
2496
2497                # --- view "Actions" lines:
2498                info.extend([
2499                    "| 1                          | 2                             | 3                            | 4                    | 5                      |\n",
2500                    "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n",
2501                    "| **Actions:**               | Trades: {:<21} | Trading volumes:             |                      |                        |\n".format(customStat["opsCount"]),
2502                    "|                            |   Buy: {:<22} | {:<28} |                      |                        |\n".format(
2503                        "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2504                        "  rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else "  —",
2505                    ),
2506                    "|                            |   Sell: {:<21} | {:<28} |                      |                        |\n".format(
2507                        "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2508                        "  rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else "  —",
2509                    ),
2510                ])
2511
2512                opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys()))))
2513                for key in opsKeys:
2514                    if key == "rub":
2515                        continue
2516
2517                    info.extend([
2518                        "|                            |                               | {:<28} |                      |                        |\n".format(
2519                            "  {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0)
2520                        ),
2521                        "|                            |                               | {:<28} |                      |                        |\n".format(
2522                            "  {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0)
2523                        ),
2524                    ])
2525
2526                info.append(splitLine1)
2527
2528                def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str:
2529                    return "|                            | {:<29} | {:<28} | {:<20} | {:<22} |\n".format(
2530                            "  {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else "  —",
2531                            "  {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else "  —",
2532                            "  {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else "  —",
2533                            "  {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else "  —",
2534                    )
2535
2536                # --- view "Payments" lines:
2537                info.append("| **Payments:**              | Deposit on broker account:    | Withdrawals:                 | Dividends income:    | Coupons income:        |\n")
2538                paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys()))))
2539
2540                for key in paymentsKeys:
2541                    info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key))
2542
2543                info.append(splitLine1)
2544
2545                # --- view "Commissions and taxes" lines:
2546                info.append("| **Commissions and taxes:** | Broker commissions:           | Service commissions:         | Margin commissions:  | All taxes/corrections: |\n")
2547                comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys()))))
2548
2549                for key in comKeys:
2550                    info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key))
2551
2552                info.append(splitLine1)
2553
2554                info.extend([
2555                    "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"),
2556                    "| Date and time       | FIGI         | Ticker       | Asset      | Value     | Payment         | Status     | Operation type                                                     |\n",
2557                    "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n",
2558                ])
2559
2560            else:
2561                info.append("Broker returned no operations during this period\n")
2562
2563            # --- view "Operations" section:
2564            for item in ops:
2565                if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]:
2566                    continue
2567
2568                else:
2569                    self.figi = item["figi"] if item["figi"] else ""
2570                    payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2571                    instrument = self.SearchByFIGI(requestPrice=False) if self.figi else {}
2572
2573                    # group of deals during one day:
2574                    if nextDay and item["date"].split("T")[0] != nextDay:
2575                        info.append(splitLine2)
2576                        nextDay = ""
2577
2578                    else:
2579                        nextDay = item["date"].split("T")[0]  # saving current day for splitting
2580
2581                    info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format(
2582                        item["date"].replace("T", " ").replace("Z", "").split(".")[0],
2583                        self.figi if self.figi else "—",
2584                        instrument["ticker"] if instrument else "—",
2585                        instrument["type"] if instrument else "—",
2586                        item["quantity"] if int(item["quantity"]) > 0 else "—",
2587                        "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—",
2588                        TKS_OPERATION_STATES[item["state"]],
2589                        TKS_OPERATION_TYPES[item["operationType"]],
2590                    ))
2591
2592            infoText = "".join(info)
2593
2594            if show:
2595                uLogger.info(infoText)
2596
2597            if self.reportFile:
2598                with open(self.reportFile, "w", encoding="UTF-8") as fH:
2599                    fH.write(infoText)
2600
2601                uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile)))
2602
2603        return ops, customStat
2604
2605    def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame:
2606        """
2607        This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id).
2608
2609        History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`.
2610        Warning! Broker server used ISO UTC time by default.
2611
2612        If `historyFile` is not `None` then method save history to file, otherwise return only pandas dataframe.
2613        Also, `historyFile` used to update history with `onlyMissing` parameter.
2614
2615        See also: `LoadHistory()` and `ShowHistoryChart()` methods.
2616
2617        :param start: see docstring in `GetDatesAsString()` method.
2618        :param end: see docstring in `GetDatesAsString()` method.
2619        :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`,
2620                         `"hour"`, `"day"`. Default: `"hour"`.
2621        :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`.
2622                            False by default. Warning! History appends only from last candle to current time
2623                            with always update last candle!
2624        :param csvSep: separator if csv-file is used, `,` by default.
2625        :param show: if `True` then also prints pandas dataframe to the console.
2626        :return: pandas dataframe with prices history. Headers of columns are defined by default:
2627                 `["date", "time", "open", "high", "low", "close", "volume"]`.
2628        """
2629        strStartDate, strEndDate = GetDatesAsString(start, end)  # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2630        headers = ["date", "time", "open", "high", "low", "close", "volume"]  # sequence and names of column headers
2631        history = None  # empty pandas object for history
2632
2633        if interval not in TKS_CANDLE_INTERVALS.keys():
2634            uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.")
2635            raise Exception("Incorrect value")
2636
2637        if not (self.ticker or self.figi):
2638            uLogger.error("Ticker or FIGI must be defined!")
2639            raise Exception("Ticker or FIGI required")
2640
2641        if self.ticker and not self.figi:
2642            instrumentByTicker = self.SearchByTicker(requestPrice=False, debug=False)
2643            self.figi = instrumentByTicker["figi"] if instrumentByTicker else ""
2644
2645        if self.figi and not self.ticker:
2646            instrumentByFIGI = self.SearchByFIGI(requestPrice=False, debug=False)
2647            self.ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else ""
2648
2649        dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from start time string
2650        dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from end time string
2651        if interval.lower() != "day":
2652            dtEnd += timedelta(seconds=1)  # adds 1 sec for requests, because day end returned by `GetDatesAsString()` as 23:59:59
2653
2654        delta = dtEnd - dtStart  # current UTC time minus last time in file
2655        deltaMinutes = delta.days * 1440 + delta.seconds // 60  # minutes between start and end dates
2656
2657        # calculate history length in candles:
2658        length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1]
2659        if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0:
2660            length += 1  # to avoid fraction time
2661
2662        # calculate data blocks count:
2663        blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2]
2664
2665        uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end))
2666        uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate))
2667        uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval))
2668        uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2]))
2669        uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self.ticker, self.figi))
2670
2671        tempOld = None  # pandas object for old history, if --only-missing key present
2672        lastTime = None  # datetime object of last old candle in file
2673
2674        if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile):
2675            uLogger.debug("--only-missing key present, add only last missing candles...")
2676            uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile)))
2677
2678            tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers)
2679
2680            tempOld["date"] = pd.to_datetime(tempOld["date"])  # load date "as is"
2681            tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d")  # convert date to string
2682            tempOld["time"] = pd.to_datetime(tempOld["time"])  # load time "as is"
2683            tempOld["time"] = tempOld["time"].dt.strftime("%H:%M")  # convert time to string
2684
2685            # get last datetime object from last string in file or minus 1 delta if file is empty:
2686            if len(tempOld) > 0:
2687                lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2688
2689            else:
2690                lastTime = dtEnd - timedelta(days=1)  # history file is empty, so last date set at -1 day
2691
2692            tempOld = tempOld[:-1]  # always remove last old candle because it may be incompletely at the current time
2693
2694        responseJSONs = []  # raw history blocks of data
2695
2696        blockEnd = dtEnd
2697        for item in range(blocks):
2698            tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2]
2699            blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail)
2700
2701            uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format(
2702                item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2703            ))
2704
2705            if blockStart == blockEnd:
2706                uLogger.debug("Skipped this zero-length block...")
2707
2708            else:
2709                # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles
2710                historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles"
2711                self.body = str({
2712                    "figi": self.figi,
2713                    "from": blockStart.strftime(TKS_DATE_TIME_FORMAT),
2714                    "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2715                    "interval": TKS_CANDLE_INTERVALS[interval][0]
2716                })
2717                responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1, debug=False)
2718
2719                if "code" in responseJSON.keys():
2720                    uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks))
2721
2722                else:
2723                    if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1:
2724                        responseJSON["candles"] = responseJSON["candles"][:-1]  # removes last candle for "yesterday" request
2725
2726                    responseJSONs = responseJSON["candles"] + responseJSONs  # add more old history behind newest dates
2727
2728            blockEnd = blockStart
2729
2730        printCount = len(responseJSONs)  # candles to show in console
2731        if responseJSONs:
2732            tempHistory = pd.DataFrame(
2733                data={
2734                    "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2735                    "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2736                    "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs],
2737                    "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs],
2738                    "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs],
2739                    "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs],
2740                    "volume": [int(item["volume"]) for item in responseJSONs],
2741                },
2742                index=range(len(responseJSONs)),
2743                columns=["date", "time", "open", "high", "low", "close", "volume"],
2744            )
2745            tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d")
2746            tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M")
2747
2748            # append only newest candles to old history if --only-missing key present:
2749            if onlyMissing and tempOld is not None and lastTime is not None:
2750                index = 0  # find start index in tempHistory data:
2751
2752                for i, item in tempHistory.iterrows():
2753                    curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2754
2755                    if curTime == lastTime:
2756                        uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
2757                        index = i
2758                        printCount = index + 1
2759                        break
2760
2761                history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True)
2762
2763            else:
2764                history = tempHistory  # if no `--only-missing` key then load full data from server
2765
2766            uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False)))
2767
2768        if history is not None and not history.empty:
2769            if show:
2770                uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format(
2771                    strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]),
2772                    pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False),
2773                ))
2774
2775        else:
2776            uLogger.warning("Received an empty candles history!")
2777
2778        if self.historyFile is not None:
2779            if history is not None and not history.empty:
2780                history.to_csv(self.historyFile, sep=csvSep, index=False, header=None)
2781                uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self.ticker, self.figi, interval, os.path.abspath(self.historyFile)))
2782
2783            else:
2784                uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile)))
2785
2786        else:
2787            uLogger.debug("--output key is not defined. Parsed history file not saved to file, only pandas dataframe returns.")
2788
2789        return history
2790
2791    def LoadHistory(self, filePath: str) -> pd.DataFrame:
2792        """
2793        Load candles history from csv-file and return pandas dataframe object.
2794
2795        See also: `History()` and `ShowHistoryChart()` methods.
2796
2797        :param filePath: path to csv-file to open.
2798        """
2799        loadedHistory = None  # init candles data object
2800
2801        uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...")
2802
2803        if os.path.exists(filePath):
2804            loadedHistory = self.priceModel.LoadFromFile(filePath)  # load data and get chain of candles as pandas dataframe
2805
2806            tfStr = self.priceModel.FormattedDelta(
2807                self.priceModel.timeframe,
2808                "{days} days {hours}h {minutes}m {seconds}s",
2809            ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta(
2810                self.priceModel.timeframe,
2811                "{hours}h {minutes}m {seconds}s",
2812            )
2813
2814            if loadedHistory is not None and not loadedHistory.empty:
2815                uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format(
2816                    len(loadedHistory),
2817                    tfStr,
2818                    pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)),
2819                )
2820
2821            else:
2822                uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath)))
2823
2824        else:
2825            uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath))
2826
2827        return loadedHistory
2828
2829    def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2830        """
2831        Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
2832
2833        Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart.
2834        Default: `index.html` (both for interact and non-interact candlesticks chart).
2835
2836        See also: `History()` and `LoadHistory()` methods.
2837
2838        :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
2839        :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart.
2840                         See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters
2841                         If False then chain of candlesticks will render as not interactive Google Candlestick chart.
2842                         See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
2843        :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to
2844                              html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file.
2845        """
2846        if isinstance(candles, str):
2847            self.priceModel.prices = self.LoadHistory(filePath=candles)  # load candles chain from file
2848            self.priceModel.ticker = os.path.basename(candles)  # use filename as ticker name in PriceGenerator
2849
2850        elif isinstance(candles, pd.DataFrame):
2851            self.priceModel.prices = candles  # set candles chain from variable
2852            self.priceModel.ticker = self.ticker  # use current TKSBrokerAPI ticker as ticker name in PriceGenerator
2853
2854            if "datetime" not in candles.columns:
2855                self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True)  # PriceGenerator uses "datetime" column with date and time
2856
2857        else:
2858            uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!")
2859            raise Exception("Incorrect value")
2860
2861        self.priceModel.horizon = len(self.priceModel.prices)  # use length of candles data as horizon in PriceGenerator
2862
2863        if interact:
2864            uLogger.debug("Rendering interactive candles chart. Wait, please...")
2865
2866            self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2867
2868        else:
2869            uLogger.debug("Rendering non-interactive candles chart. Wait, please...")
2870
2871            self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2872
2873        uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))
2874
2875    def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2876        """
2877        Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response.
2878        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2879
2880        See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`.
2881
2882        :param operation: string "Buy" or "Sell".
2883        :param lots: volume, integer count of lots >= 1.
2884        :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`.
2885        :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`.
2886        :param expDate: string "Undefined" by default or local date in future,
2887                        it is a string with format `%Y-%m-%d %H:%M:%S`.
2888        :return: JSON with response from broker server.
2889        """
2890        if self.accountId is None or not self.accountId:
2891            uLogger.error("Variable `accountId` must be defined for using this method!")
2892            raise Exception("Account ID required")
2893
2894        if operation is None or not operation or operation not in ("Buy", "Sell"):
2895            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
2896            raise Exception("Incorrect value")
2897
2898        if lots is None or lots < 1:
2899            uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.")
2900            lots = 1
2901
2902        if tp is None or tp < 0:
2903            tp = 0
2904
2905        if sl is None or sl < 0:
2906            sl = 0
2907
2908        if expDate is None or not expDate:
2909            expDate = "Undefined"
2910
2911        if not (self.ticker or self.figi):
2912            uLogger.error("Ticker or FIGI must be defined!")
2913            raise Exception("Ticker or FIGI required")
2914
2915        instrument = self.SearchByTicker(requestPrice=True, debug=False) if self.ticker else self.SearchByFIGI(requestPrice=True, debug=False)
2916        self.ticker = instrument["ticker"]
2917        self.figi = instrument["figi"]
2918
2919        uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self.ticker, self.figi, lots, tp, sl, expDate))
2920
2921        openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
2922        self.body = str({
2923            "figi": self.figi,
2924            "quantity": str(lots),
2925            "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
2926            "accountId": str(self.accountId),
2927            "orderType": "ORDER_TYPE_MARKET",  # see: TKS_ORDER_TYPES
2928        })
2929        response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0, debug=False)
2930
2931        if "orderId" in response.keys():
2932            uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format(
2933                operation, response["orderId"],
2934                self.ticker, self.figi, lots,
2935                NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"],
2936                NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"],
2937                NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"],
2938            ))
2939
2940        else:
2941            uLogger.warning("Not `oK` status received! Market order not created. See full debug log or try again and open order later.")
2942
2943        if tp > 0:
2944            self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate)
2945
2946        if sl > 0:
2947            self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate)
2948
2949        return response
2950
2951    def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2952        """
2953        More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response.
2954        If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter.
2955
2956        See also: `Order()` and `Trade()` docstrings.
2957
2958        :param lots: volume, integer count of lots >= 1.
2959        :param tp: float > 0, take profit price of stop-order.
2960        :param sl: float > 0, stop loss price of stop-order.
2961        :param expDate: it's a local date in future.
2962                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
2963        :return: JSON with response from broker server.
2964        """
2965        return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)
2966
2967    def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2968        """
2969        More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response.
2970        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2971
2972        See also: `Order()` and `Trade()` docstrings.
2973
2974        :param lots: volume, integer count of lots >= 1.
2975        :param tp: float > 0, take profit price of stop-order.
2976        :param sl: float > 0, stop loss price of stop-order.
2977        :param expDate: it's a local date in the future.
2978                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
2979        :return: JSON with response from broker server.
2980        """
2981        return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)
2982
2983    def CloseTrades(self, tickers: list, portfolio: dict = None) -> None:
2984        """
2985        Close position of given instruments.
2986
2987        :param tickers: tickers list of instruments that must be closed.
2988        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
2989                         This avoids unnecessary downloading data from the server.
2990        """
2991        if not tickers:
2992            uLogger.info("Tickers list is empty, nothing to close.")
2993
2994        else:
2995            if portfolio is None or not portfolio:
2996                portfolio = self.Overview(show=False)
2997
2998            allOpenedTickers = [item["ticker"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]]
2999            uLogger.debug("All opened instruments by it's tickers names: {}".format(allOpenedTickers))
3000
3001            for ticker in tickers:
3002                if ticker not in allOpenedTickers:
3003                    uLogger.warning("Instrument with ticker [{}] not in open positions list!".format(ticker))
3004                    continue
3005
3006                # search open trade info about instrument by ticker:
3007                instrument = {}
3008                for iType in TKS_INSTRUMENTS:
3009                    if instrument:
3010                        break
3011
3012                    for item in portfolio["stat"][iType]:
3013                        if item["ticker"] == ticker:
3014                            instrument = item
3015                            break
3016
3017                if instrument:
3018                    self.ticker = ticker
3019                    self.figi = instrument["figi"]
3020
3021                    uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format(
3022                        self.ticker,
3023                        self.figi,
3024                        int(instrument["volume"]),
3025                        ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "",
3026                    ))
3027
3028                    tradeLots = abs(instrument["lots"]) - instrument["blocked"]  # available volumes in lots for close operation
3029
3030                    if tradeLots > 0:
3031                        if instrument["blocked"] > 0:
3032                            uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format(
3033                                instrument["blocked"],
3034                                self.ticker,
3035                                tradeLots,
3036                            ))
3037
3038                        # if direction is "Long" then we need sell, if direction is "Short" then we need buy:
3039                        self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots)
3040
3041                    else:
3042                        uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self.ticker))
3043
3044    def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3045        """
3046        Close all positions of given instruments with defined type.
3047
3048        :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
3049        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3050                         This avoids unnecessary downloading data from the server.
3051        """
3052        if iType not in TKS_INSTRUMENTS:
3053            uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType))
3054
3055        else:
3056            if portfolio is None or not portfolio:
3057                portfolio = self.Overview(show=False)
3058
3059            tickers = [item["ticker"] for item in portfolio["stat"][iType]]
3060            uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers))
3061
3062            if tickers and portfolio:
3063                self.CloseTrades(tickers, portfolio)
3064
3065            else:
3066                uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))
3067
3068    def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3069        """
3070        Universal method to create market or limit orders with all available parameters for current `accountId`.
3071        See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`.
3072
3073        If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above
3074        current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
3075
3076        Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell"
3077        then broker immediately open market order as you can do simple --buy or --sell operations!
3078
3079        If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell".
3080        When current price will go up or down to target price value then broker opens a limit order.
3081        Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
3082
3083        Only one attempt and no retry for opens order. If network issue occurred you can create new request.
3084
3085        :param operation: string "Buy" or "Sell".
3086        :param orderType: string "Limit" or "Stop".
3087        :param lots: volume, integer count of lots >= 1.
3088        :param targetPrice: target price > 0. This is open trade price for limit order.
3089        :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice.
3090                           Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
3091        :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types
3092                         "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3093                         Stop loss order always executed by market price.
3094        :param expDate: string "Undefined" by default or local date in future.
3095                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3096                        This date is converting to UTC format for server. This parameter only makes sense for stop-order.
3097                        A limit order has no expiration date, it lasts until the end of the trading day.
3098        :return: JSON with response from broker server.
3099        """
3100        if self.accountId is None or not self.accountId:
3101            uLogger.error("Variable `accountId` must be defined for using this method!")
3102            raise Exception("Account ID required")
3103
3104        if operation is None or not operation or operation not in ("Buy", "Sell"):
3105            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
3106            raise Exception("Incorrect value")
3107
3108        if orderType is None or not orderType or orderType not in ("Limit", "Stop"):
3109            uLogger.error("You must define order type only one of them: `Limit` or `Stop`!")
3110            raise Exception("Incorrect value")
3111
3112        if lots is None or lots < 1:
3113            uLogger.error("You must define trade volume > 0: integer count of lots!")
3114            raise Exception("Incorrect value")
3115
3116        if targetPrice is None or targetPrice <= 0:
3117            uLogger.error("Target price for limit-order must be greater than 0!")
3118            raise Exception("Incorrect value")
3119
3120        if limitPrice is None or limitPrice <= 0:
3121            limitPrice = targetPrice
3122
3123        if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"):
3124            stopType = "Limit"
3125
3126        if expDate is None or not expDate:
3127            expDate = "Undefined"
3128
3129        if not (self.ticker or self.figi):
3130            uLogger.error("Tocker or FIGI must be defined!")
3131            raise Exception("Ticker or FIGI required")
3132
3133        response = {}
3134        instrument = self.SearchByTicker(requestPrice=True, debug=False) if self.ticker else self.SearchByFIGI(requestPrice=True, debug=False)
3135        self.ticker = instrument["ticker"]
3136        self.figi = instrument["figi"]
3137
3138        if orderType == "Limit":
3139            uLogger.debug(
3140                "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format(
3141                    self.ticker, self.figi,
3142                    operation, lots, targetPrice, instrument["currency"],
3143                ))
3144
3145            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3146            self.body = str({
3147                "figi": self.figi,
3148                "quantity": str(lots),
3149                "price": FloatToNano(targetPrice),
3150                "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3151                "accountId": str(self.accountId),
3152                "orderType": "ORDER_TYPE_LIMIT",  # see: TKS_ORDER_TYPES
3153            })
3154            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0, debug=False)
3155
3156            if "orderId" in response.keys():
3157                uLogger.info(
3158                    "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format(
3159                        response["orderId"],
3160                        self.ticker, self.figi,
3161                        operation, lots, targetPrice, instrument["currency"],
3162                    ))
3163
3164                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3165                    if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]:
3166                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format(
3167                            targetPrice, instrument["currency"],
3168                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3169                        ))
3170
3171                    if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]:
3172                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format(
3173                            targetPrice, instrument["currency"],
3174                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3175                        ))
3176
3177            else:
3178                uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log or try again and open order later.")
3179
3180        if orderType == "Stop":
3181            uLogger.debug(
3182                "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format(
3183                    self.ticker, self.figi,
3184                    operation, lots,
3185                    targetPrice, instrument["currency"],
3186                    limitPrice, instrument["currency"],
3187                    stopType, expDate,
3188                ))
3189
3190            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder"
3191            expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT)
3192            stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT"
3193
3194            body = {
3195                "figi": self.figi,
3196                "quantity": str(lots),
3197                "price": FloatToNano(limitPrice),
3198                "stopPrice": FloatToNano(targetPrice),
3199                "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL",  # see: TKS_STOP_ORDER_DIRECTIONS
3200                "accountId": str(self.accountId),
3201                "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL",  # see: TKS_STOP_ORDER_EXPIRATION_TYPES
3202                "stopOrderType": stopOrderType,  # see: TKS_STOP_ORDER_TYPES
3203            }
3204
3205            if expDateUTC:
3206                body["expireDate"] = expDateUTC
3207
3208            self.body = str(body)
3209            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0, debug=False)
3210
3211            if "stopOrderId" in response.keys():
3212                uLogger.info(
3213                    "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format(
3214                        response["stopOrderId"],
3215                        self.ticker, self.figi,
3216                        operation, lots,
3217                        targetPrice, instrument["currency"],
3218                        limitPrice, instrument["currency"],
3219                        TKS_STOP_ORDER_TYPES[stopOrderType],
3220                        datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"],
3221                    ))
3222
3223                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3224                    if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3225                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3226                            targetPrice, instrument["currency"],
3227                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3228                        ))
3229
3230                    if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3231                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3232                            targetPrice, instrument["currency"],
3233                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3234                        ))
3235
3236            else:
3237                uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log or try again and open order later.")
3238
3239        return response
3240
3241    def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3242        """
3243        Create pending `Buy` limit-order (below current price). You must specify only 2 parameters:
3244        `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then
3245        broker immediately open `Buy` market order, such as if you do simple `--buy` operation!
3246        See also: `Order()` docstring.
3247
3248        :param lots: volume, integer count of lots >= 1.
3249        :param targetPrice: target price > 0. This is open trade price for limit order.
3250        :return: JSON with response from broker server.
3251        """
3252        return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)
3253
3254    def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3255        """
3256        Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order.
3257        In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3258        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3259        target price value then broker opens a limit order. See also: `Order()` docstring.
3260
3261        :param lots: volume, integer count of lots >= 1.
3262        :param targetPrice: target price > 0. This is trigger price for buy stop-order.
3263        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3264                           with price equal to limitPrice, when current price goes to target price of buy stop-order.
3265        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3266                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3267        :param expDate: string "Undefined" by default or local date in future.
3268                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3269                        This date is converting to UTC format for server.
3270        :return: JSON with response from broker server.
3271        """
3272        return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3273
3274    def SellLimit(self, lots: int, targetPrice: float) -> dict:
3275        """
3276        Create pending `Sell` limit-order (above current price). You must specify only 2 parameters:
3277        `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then
3278        broker immediately open `Sell` market order, such as if you do simple `--sell` operation!
3279        See also: `Order()` docstring.
3280
3281        :param lots: volume, integer count of lots >= 1.
3282        :param targetPrice: target price > 0. This is open trade price for limit order.
3283        :return: JSON with response from broker server.
3284        """
3285        return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)
3286
3287    def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3288        """
3289        Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order.
3290        In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3291        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3292        target price value then broker opens a limit order. See also: `Order()` docstring.
3293
3294        :param lots: volume, integer count of lots >= 1.
3295        :param targetPrice: target price > 0. This is trigger price for sell stop-order.
3296        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3297                           with price equal to limitPrice, when current price goes to target price of sell stop-order.
3298        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3299                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3300        :param expDate: string "Undefined" by default or local date in future.
3301                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3302                        This date is converting to UTC format for server.
3303        :return: JSON with response from broker server.
3304        """
3305        return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3306
3307    def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3308        """
3309        Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`.
3310
3311        :param orderIDs: list of integers with `orderId` or `stopOrderId`.
3312        :param allOrdersIDs: pre-received lists of all active pending orders.
3313                             This avoids unnecessary downloading data from the server.
3314        :param allStopOrdersIDs: pre-received lists of all active stop orders.
3315        """
3316        if self.accountId is None or not self.accountId:
3317            uLogger.error("Variable `accountId` must be defined for using this method!")
3318            raise Exception("Account ID required")
3319
3320        if orderIDs:
3321            if allOrdersIDs is None or not allOrdersIDs:
3322                rawOrders = self.RequestPendingOrders()
3323                allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending orders ID
3324
3325            if allStopOrdersIDs is None or not allStopOrdersIDs:
3326                rawStopOrders = self.RequestStopOrders()
3327                allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3328
3329            for orderID in orderIDs:
3330                idInPendingOrders = orderID in allOrdersIDs
3331                idInStopOrders = orderID in allStopOrdersIDs
3332
3333                if not (idInPendingOrders or idInStopOrders):
3334                    uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID))
3335                    continue
3336
3337                else:
3338                    if idInPendingOrders:
3339                        uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID))
3340
3341                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder
3342                        self.body = str({"accountId": self.accountId, "orderId": orderID})
3343                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder"
3344                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3345
3346                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3347                            uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3348                            uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID))
3349
3350                        else:
3351                            uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID))
3352
3353                    elif idInStopOrders:
3354                        uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID))
3355
3356                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder
3357                        self.body = str({"accountId": self.accountId, "stopOrderId": orderID})
3358                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder"
3359                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3360
3361                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3362                            uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3363                            uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID))
3364
3365                        else:
3366                            uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID))
3367
3368                    else:
3369                        continue
3370
3371    def CloseAllOrders(self) -> None:
3372        """
3373        Gets a list of open pending and stop orders and cancel it all.
3374        """
3375        rawOrders = self.RequestPendingOrders()
3376        allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending orders ID
3377        lenOrders = len(allOrdersIDs)
3378
3379        rawStopOrders = self.RequestStopOrders()
3380        allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3381        lenSOrders = len(allStopOrdersIDs)
3382
3383        if lenOrders > 0 or lenSOrders > 0:
3384            uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders))
3385
3386            self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs)
3387
3388        else:
3389            uLogger.info("Orders not found, nothing to cancel.")
3390
3391    def CloseAll(self, *args) -> None:
3392        """
3393        Close all available (not blocked) opened trades and orders.
3394
3395        Also, you can select one or more keywords case-insensitive:
3396        `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type.
3397
3398        Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods.
3399        """
3400        overview = self.Overview(show=False)  # get all open trades info
3401
3402        if len(args) == 0:
3403            uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...")
3404            self.CloseAllOrders()  # close all pending and stop orders
3405
3406            for iType in TKS_INSTRUMENTS:
3407                if iType != "Currencies":
3408                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3409
3410        else:
3411            uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args)))
3412            lowerArgs = [x.lower() for x in args]
3413
3414            if "orders" in lowerArgs:
3415                self.CloseAllOrders()  # close all pending and stop orders
3416
3417            for iType in TKS_INSTRUMENTS:
3418                if iType.lower() in lowerArgs and iType != "Currencies":
3419                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3420
3421    @staticmethod
3422    def ParseOrderParameters(operation, **inputParameters):
3423        """
3424        Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
3425
3426        :param operation: string "Buy" or "Sell".
3427        :param inputParameters: this is dict of strings that looks like this
3428               `{"lots": "L_int,...", "prices": "P_float,..."}` where
3429               "lots" key: one or more lot values (integer numbers) to open with every limit-order
3430               "prices" key: one or more prices to open limit-orders
3431               Counts of values in lots and prices lists must be equals!
3432        :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]`
3433        """
3434        # TODO: update order grid work with api v2
3435        pass
3436        # uLogger.debug("Input parameters: {}".format(inputParameters))
3437        #
3438        # if operation is None or not operation or operation not in ("Buy", "Sell"):
3439        #     uLogger.error("You must define operation type: 'Buy' or 'Sell'!")
3440        #     raise Exception("Incorrect value")
3441        #
3442        # if "l" in inputParameters.keys():
3443        #     inputParameters["lots"] = inputParameters.pop("l")
3444        #
3445        # if "p" in inputParameters.keys():
3446        #     inputParameters["prices"] = inputParameters.pop("p")
3447        #
3448        # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys():
3449        #     uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!")
3450        #     raise Exception("Incorrect value")
3451        #
3452        # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")]
3453        # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")]
3454        #
3455        # if len(lots) != len(prices):
3456        #     uLogger.error("'lots' and 'prices' lists must have equal length of values!")
3457        #     raise Exception("Incorrect value")
3458        #
3459        # uLogger.debug("Extracted parameters for orders:")
3460        # uLogger.debug("lots = {}".format(lots))
3461        # uLogger.debug("prices = {}".format(prices))
3462        #
3463        # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...]
3464        # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))]
3465        # uLogger.debug("Order parameters: {}".format(result))
3466        #
3467        # return result
3468
3469    def IsInPortfolio(self, portfolio: dict = None) -> bool:
3470        """
3471        Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`.
3472
3473        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3474        :return: `True` if portfolio contains open position with given instrument, `False` otherwise.
3475        """
3476        result = False
3477        msg = "Instrument not defined!"
3478
3479        if portfolio is None or not portfolio:
3480            portfolio = self.Overview(show=False)
3481
3482        if self.ticker:
3483            uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker))
3484            msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker)
3485
3486            for iType in TKS_INSTRUMENTS:
3487                for instrument in portfolio["stat"][iType]:
3488                    if instrument["ticker"] == self.ticker:
3489                        result = True
3490                        msg = "Instrument with ticker [{}] is present in open positions".format(self.ticker)
3491                        break
3492
3493        elif self.figi:
3494            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi))
3495            msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi)
3496
3497            for iType in TKS_INSTRUMENTS:
3498                for instrument in portfolio["stat"][iType]:
3499                    if instrument["figi"] == self.figi:
3500                        result = True
3501                        msg = "Instrument with FIGI [{}] is present in open positions".format(self.figi)
3502                        break
3503
3504        else:
3505            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3506
3507        uLogger.debug(msg)
3508
3509        return result
3510
3511    def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3512        """
3513        Returns instrument is in the user's portfolio if it presents there.
3514        Instrument must be defined by `ticker` (highly priority) or `figi`.
3515
3516        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3517        :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise.
3518        """
3519        result = None
3520        msg = "Instrument not defined!"
3521
3522        if portfolio is None or not portfolio:
3523            portfolio = self.Overview(show=False)
3524
3525        if self.ticker:
3526            uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker))
3527            msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker)
3528
3529            for iType in TKS_INSTRUMENTS:
3530                for instrument in portfolio["stat"][iType]:
3531                    if instrument["ticker"] == self.ticker:
3532                        result = instrument
3533                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self.ticker, instrument["figi"])
3534                        break
3535
3536        elif self.figi:
3537            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi))
3538            msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi)
3539
3540            for iType in TKS_INSTRUMENTS:
3541                for instrument in portfolio["stat"][iType]:
3542                    if instrument["figi"] == self.figi:
3543                        result = instrument
3544                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self.figi)
3545                        break
3546
3547        else:
3548            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3549
3550        uLogger.debug(msg)
3551
3552        return result
3553
3554    def RequestLimits(self) -> dict:
3555        """
3556        Method for obtaining the available funds for withdrawal for current `accountId`.
3557
3558        See also:
3559        - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
3560        - `OverviewLimits()` method
3561
3562        :return: dict with raw data from server that contains free funds for withdrawal. Example of dict:
3563                 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`.
3564                 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency
3565                 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures.
3566        """
3567        if self.accountId is None or not self.accountId:
3568            uLogger.error("Variable `accountId` must be defined for using this method!")
3569            raise Exception("Account ID required")
3570
3571        uLogger.debug("Requesting current available funds for withdrawal. Wait, please...")
3572
3573        self.body = str({"accountId": self.accountId})
3574        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits"
3575        rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3576
3577        uLogger.debug("Records about available funds for withdrawal successfully received")
3578
3579        return rawLimits
3580
3581    def OverviewLimits(self, show: bool = False) -> dict:
3582        """
3583        Method for parsing and show table with available funds for withdrawal for current `accountId`.
3584
3585        See also: `RequestLimits()`.
3586
3587        :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log.
3588        :return: dict with raw parsed data from server and some calculated statistics about it.
3589        """
3590        if self.accountId is None or not self.accountId:
3591            uLogger.error("Variable `accountId` must be defined for using this method!")
3592            raise Exception("Account ID required")
3593
3594        rawLimits = self.RequestLimits()  # raw response with current available funds for withdrawal
3595
3596        view = {
3597            "rawLimits": rawLimits,
3598            "limits": {  # parsed data for every currency:
3599                "money": {  # this is an array of portfolio currency positions
3600                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"]
3601                },
3602                "blocked": {  # this is an array of blocked currency
3603                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"]
3604                },
3605                "blockedGuarantee": {  # this is locked money under collateral for futures
3606                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"]
3607                },
3608            },
3609        }
3610
3611        # --- Prepare text table with limits in human-readable format:
3612        if show:
3613            info = [
3614                "# Withdrawal limits\n\n",
3615                "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
3616                "* **Account ID:** [{}]\n".format(self.accountId),
3617                "\n| Currencies | Total         | Available for withdrawal | Blocked for trade | Futures guarantee |\n",
3618                "|------------|---------------|--------------------------|-------------------|-------------------|\n",
3619            ]
3620
3621            for curr in view["limits"]["money"].keys():
3622                blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0
3623                blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0
3624                availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee)
3625
3626                infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format(
3627                    "[{}]".format(curr),
3628                    "{:.2f}".format(view["limits"]["money"][curr]),
3629                    "{:.2f}".format(availableMoney),
3630                    "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—",
3631                    "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—",
3632                )
3633
3634                if curr == "rub":
3635                    info.insert(5, infoStr)  # insert at first position in table and after headers
3636
3637                else:
3638                    info.append(infoStr)
3639
3640            infoText = "".join(info)
3641
3642            uLogger.info(infoText)
3643
3644            if self.withdrawalLimitsFile:
3645                with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH:
3646                    fH.write(infoText)
3647
3648                uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile)))
3649
3650        return view
3651
3652    def RequestAccounts(self) -> dict:
3653        """
3654        Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`.
3655
3656        See also:
3657        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
3658        - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
3659        - `OverviewUserInfo()` method
3660
3661        :return: dict with raw data from server that contains accounts info. Example of dict:
3662                 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account",
3663                   "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z",
3664                   "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`.
3665                 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now.
3666        """
3667        uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...")
3668
3669        self.body = str({})
3670        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts"
3671        rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST")
3672
3673        uLogger.debug("Records about available accounts successfully received")
3674
3675        return rawAccounts
3676
3677    def RequestUserInfo(self) -> dict:
3678        """
3679        Method for requesting common user's information.
3680
3681        See also:
3682        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
3683        - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
3684        - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with
3685        - `OverviewUserInfo()` method
3686
3687        :return: dict with raw data from server that contains user's information. Example of dict:
3688                 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage",
3689                   "russian_shares", "structured_income_bonds"], "tariff": "premium"}`.
3690        """
3691        uLogger.debug("Requesting common user's information. Wait, please...")
3692
3693        self.body = str({})
3694        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo"
3695        rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST")
3696
3697        uLogger.debug("Records about current user successfully received")
3698
3699        return rawUserInfo
3700
3701    def RequestMarginStatus(self, accountId: str = None) -> dict:
3702        """
3703        Method for requesting margin calculation for defined account ID.
3704
3705        See also:
3706        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
3707        - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
3708        - `OverviewUserInfo()` method
3709
3710        :param accountId: string with numeric account ID. If `None`, then used class field `accountId`.
3711        :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict.
3712                 Example of responses:
3713                 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`.
3714                 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000},
3715                                    "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000},
3716                                    "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000},
3717                                    "fundsSufficiencyLevel": {"units": "1", "nano": 280000000},
3718                                    "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`.
3719        """
3720        if accountId is None or not accountId:
3721            if self.accountId is None or not self.accountId:
3722                uLogger.error("Variable `accountId` must be defined for using this method!")
3723                raise Exception("Account ID required")
3724
3725            else:
3726                accountId = self.accountId  # use `self.accountId` (main ID) by default
3727
3728        uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId))
3729
3730        self.body = str({"accountId": accountId})
3731        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes"
3732        rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST")
3733
3734        if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}:
3735            uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId))
3736            rawMargin = {}
3737
3738        else:
3739            uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId))
3740
3741        return rawMargin
3742
3743    def RequestTariffLimits(self) -> dict:
3744        """
3745        Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`.
3746
3747        See also:
3748        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
3749        - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
3750        - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
3751        - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
3752        - `OverviewUserInfo()` method
3753
3754        :return: dict with raw data from server that contains limits of current tariff. Example of dict:
3755                 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...],
3756                   "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`.
3757        """
3758        uLogger.debug("Requesting limits of current tariff. Wait, please...")
3759
3760        self.body = str({})
3761        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff"
3762        rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3763
3764        uLogger.debug("Records with limits of current tariff successfully received")
3765
3766        return rawTariffLimits
3767
3768    def RequestBondCoupons(self, iJSON: dict) -> dict:
3769        """
3770        Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
3771        then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`.
3772        All dates are in UTC timezone.
3773
3774        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons
3775        Documentation:
3776        - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
3777        - response: https://tinkoff.github.io/investAPI/instruments/#coupon
3778
3779        See also: `ExtendBondsData()`.
3780
3781        :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self.ticker]`
3782                      If raw iJSON is not data of bond then server returns an error [400] with message:
3783                      `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`.
3784        :return: dictionary with bond payment calendar. Response example
3785                 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12",
3786                   "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000},
3787                   "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z",
3788                   "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}`
3789        """
3790        if iJSON["figi"] is None or not iJSON["figi"]:
3791            uLogger.error("FIGI must be defined for using this method!")
3792            raise Exception("FIGI required")
3793
3794        startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z"
3795        endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z"
3796
3797        uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format(
3798            "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "",
3799            self.figi,
3800            startDate,
3801            endDate,
3802        ))
3803
3804        self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate})
3805        calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons"
3806        calendar = self.SendAPIRequest(calendarURL, reqType="POST", debug=False)
3807
3808        if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}:
3809            uLogger.warning("Instrument type is not bond!")
3810
3811        else:
3812            uLogger.debug("Records about bond payment calendar successfully received")
3813
3814        return calendar
3815
3816    def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame:
3817        """
3818        Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider
3819        pandas dataframe with more information about bonds: main info, current prices, bond payment calendar,
3820        coupon yields, current yields and some statistics etc.
3821
3822        WARNING! This is too long operation if a lot of bonds requested from broker server.
3823
3824        See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`.
3825
3826        :param instruments: list of strings with tickers or FIGIs.
3827        :param xlsx: if True then also exports pandas dataframe to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`,
3828                     for further used by data scientists or stock analytics.
3829        :return: wider pandas dataframe with more full and calculated data about bonds, than raw response from broker.
3830                 In XLSX-file and pandas dataframe fields mean:
3831                 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond
3832                 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
3833        """
3834        if instruments is None or not instruments:
3835            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
3836            raise Exception("Ticker or FIGI required")
3837
3838        if isinstance(instruments, str):
3839            instruments = [instruments]
3840
3841        uniqueInstruments = self.GetUniqueFIGIs(instruments)
3842
3843        uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...")
3844
3845        iCount = len(uniqueInstruments)
3846        tooLong = iCount >= 20
3847        if tooLong:
3848            uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...")
3849
3850        bonds = None
3851        for i, self.figi in enumerate(uniqueInstruments):
3852            instrument = self.SearchByFIGI(requestPrice=False)  # raw data about instrument from server
3853
3854            if "type" in instrument.keys() and instrument["type"] == "Bonds":
3855                # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond
3856                rawBond = self.SearchByFIGI(requestPrice=True)
3857
3858                # Widen raw data with UTC current time (iData["actualDateTime"]):
3859                actualDate = datetime.now(tzutc())
3860                iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond
3861
3862                # Widen raw data with bond payment calendar (iData["rawCalendar"]):
3863                iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)}
3864
3865                # Replace some values with human-readable:
3866                iData["nominalCurrency"] = iData["nominal"]["currency"]
3867                iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"])
3868                iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"])
3869                iData["aciCurrency"] = iData["aciValue"]["currency"]
3870                iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"])
3871                iData["issueSize"] = int(iData["issueSize"])
3872                iData["issueSizePlan"] = int(iData["issueSizePlan"])
3873                iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]]
3874                iData["step"] = iData["step"] if "step" in iData.keys() else 0
3875                iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]]
3876                iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0
3877                iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0
3878                iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0
3879                iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0
3880                iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0
3881                iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0
3882
3883                # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date):
3884                iData["limitUpPercent"] = iData["currentPrice"]["limitUp"]  # max price on current day in percents of nominal
3885                iData["limitDownPercent"] = iData["currentPrice"]["limitDown"]  # min price on current day in percents of nominal
3886                iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"]  # last price on market in percents of nominal
3887                iData["closePricePercent"] = iData["currentPrice"]["closePrice"]  # previous day close in percents of nominal
3888                iData["changes"] = iData["currentPrice"]["changes"]  # this is percent of changes between `currentPrice` and `lastPrice`
3889                iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100  # max price on current day is `limitUpPercent` * `nominal`
3890                iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100  # min price on current day is `limitDownPercent` * `nominal`
3891                iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100  # last price on market is `lastPricePercent` * `nominal`
3892                iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100  # previous day close is `closePricePercent` * `nominal`
3893                iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"]  # this is delta between last deal price and last close
3894
3895                # Widen raw data with calendar data from `rawCalendar` values:
3896                calendarData = []
3897                for item in iData["rawCalendar"]["events"]:
3898                    calendarData.append({
3899                        "couponDate": item["couponDate"],
3900                        "couponNumber": int(item["couponNumber"]),
3901                        "fixDate": item["fixDate"] if "fixDate" in item.keys() else "",
3902                        "payCurrency": item["payOneBond"]["currency"],
3903                        "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]),
3904                        "couponType": TKS_COUPON_TYPES[item["couponType"]],
3905                        "couponStartDate": item["couponStartDate"],
3906                        "couponEndDate": item["couponEndDate"],
3907                        "couponPeriod": item["couponPeriod"],
3908                    })
3909
3910                # if maturity date is unknown then uses the latest date in bond payment calendar for it:
3911                if "maturityDate" not in iData.keys():
3912                    iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else ""
3913
3914                # Widen raw data with Coupon Rate.
3915                # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%:
3916                iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData])
3917                iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData])
3918                iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0.
3919
3920                # Widen raw data with Yield to Maturity (YTM) on current date.
3921                # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%:
3922                maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None
3923                iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None
3924                iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate])
3925                iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"]  # sum of all last coupons minus current ACI value
3926                iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0.
3927
3928                iData["calendar"] = calendarData  # adds calendar at the end
3929
3930                # Remove not used data:
3931                iData.pop("uid")
3932                iData.pop("positionUid")
3933                iData.pop("currentPrice")
3934                iData.pop("rawCalendar")
3935
3936                colNames = list(iData.keys())
3937                if bonds is None:
3938                    bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames))
3939
3940                else:
3941                    bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True)
3942
3943            else:
3944                uLogger.warning("Instrument with ticker [{}] and FIGI [{}] is not a bond!".format(instrument["ticker"], instrument["figi"]))
3945
3946            processed = round(100 * (i + 1) / iCount, 1)
3947            if tooLong and processed % 5 == 0:
3948                uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount))
3949
3950            else:
3951                uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount))
3952
3953        bonds.index = bonds["ticker"].tolist()  # replace indexes with ticker names
3954
3955        # Saving bonds from pandas dataframe to XLSX sheet:
3956        if xlsx and self.bondsXLSXFile:
3957            with pd.ExcelWriter(
3958                    path=self.bondsXLSXFile,
3959                    date_format=TKS_DATE_FORMAT,
3960                    datetime_format=TKS_DATE_TIME_FORMAT,
3961                    mode="w",
3962            ) as writer:
3963                bonds.to_excel(
3964                    writer,
3965                    sheet_name="Extended bonds data",
3966                    index=True,
3967                    encoding="UTF-8",
3968                    freeze_panes=(1, 1),
3969                )  # saving as XLSX-file with freeze first row and column as headers
3970
3971            uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile)))
3972
3973        return bonds
3974
3975    def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame:
3976        """
3977        Creates bond payments calendar as pandas dataframe, and also save it to the XLSX-file, `calendar.xlsx` by default.
3978
3979        WARNING! This is too long operation if a lot of bonds requested from broker server.
3980
3981        See also: `ShowBondsCalendar()`, `ExtendBondsData()`.
3982
3983        :param extBonds: pandas dataframe object returns by `ExtendBondsData()` method and contains
3984                        extended information about bonds: main info, current prices, bond payment calendar,
3985                        coupon yields, current yields and some statistics etc.
3986                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
3987        :param xlsx: if True then also exports pandas dataframe to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default,
3988                     for further used by data scientists or stock analytics.
3989        :return: pandas dataframe with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
3990        """
3991        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
3992            extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False)
3993
3994        uLogger.debug("Generating bond payments calendar data. Wait, please...")
3995
3996        colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"]
3997        colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"]
3998        calendar = None
3999        for bond in extBonds.iterrows():
4000            for item in bond[1]["calendar"]:
4001                cData = {
4002                    "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()),
4003                    "couponDate": item["couponDate"],
4004                    "figi": bond[1]["figi"],
4005                    "ticker": bond[1]["ticker"],
4006                    "name": bond[1]["name"],
4007                    "couponNumber": item["couponNumber"],
4008                    "payOneBond": item["payOneBond"],
4009                    "payCurrency": item["payCurrency"],
4010                    "couponType": item["couponType"],
4011                    "couponPeriod": item["couponPeriod"],
4012                    "fixDate": item["fixDate"],
4013                    "couponStartDate": item["couponStartDate"],
4014                    "couponEndDate": item["couponEndDate"],
4015                }
4016
4017                if calendar is None:
4018                    calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID))
4019
4020                else:
4021                    calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True)
4022
4023        calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True)  # sort all payments for all bonds by payment date
4024
4025        # Saving calendar from pandas dataframe to XLSX sheet:
4026        if xlsx:
4027            xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx"
4028
4029            with pd.ExcelWriter(
4030                    path=xlsxCalendarFile,
4031                    date_format=TKS_DATE_FORMAT,
4032                    datetime_format=TKS_DATE_TIME_FORMAT,
4033                    mode="w",
4034            ) as writer:
4035                humanReadable = calendar.copy(deep=True)
4036                humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0])
4037                humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0])
4038                humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0])
4039                humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0])
4040                humanReadable.columns = colNames  # human-readable column names
4041
4042                humanReadable.to_excel(
4043                    writer,
4044                    sheet_name="Bond payments calendar",
4045                    index=False,
4046                    encoding="UTF-8",
4047                    freeze_panes=(1, 2),
4048                )  # saving as XLSX-file with freeze first row and column as headers
4049
4050                del humanReadable  # release df in memory
4051
4052            uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile)))
4053
4054        return calendar
4055
4056    def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str:
4057        """
4058        Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond.
4059        Also, creates Markdown file with calendar data, `calendar.md` by default.
4060
4061        See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`.
4062
4063        :param extBonds: pandas dataframe object returns by `ExtendBondsData()` method and contains
4064                        extended information about bonds: main info, current prices, bond payment calendar,
4065                        coupon yields, current yields and some statistics etc.
4066                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4067        :param show: if `True` then also printing bonds payment calendar to the console,
4068                     otherwise save to file `calendarFile` only. `False` by default.
4069        :return: multilines text in Markdown format with bonds payment calendar as a table.
4070        """
4071        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4072            extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False)
4073
4074        infoText = "# Bond payments calendar\n\n"
4075
4076        calendar = self.CreateBondsCalendar(extBonds, xlsx=True)  # generate pandas dataframe with full calendar data
4077
4078        if not calendar.empty:
4079            splitLine = "|       |                 |              |              |     |               |           |        |                   |\n"
4080
4081            info = [
4082                "| Paid  | Payment date    | FIGI         | Ticker       | No. | Value         | Type      | Period | End registry date |\n",
4083                "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n",
4084            ]
4085
4086            newMonth = False
4087            notOneBond = calendar["figi"].nunique() > 1
4088            for i, bond in enumerate(calendar.iterrows()):
4089                if newMonth and notOneBond:
4090                    info.append(splitLine)
4091
4092                info.append(
4093                    "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format(
4094                        "  √" if bond[1]["paid"] else "  —",
4095                        bond[1]["couponDate"].split("T")[0],
4096                        bond[1]["figi"],
4097                        bond[1]["ticker"],
4098                        bond[1]["couponNumber"],
4099                        "{} {}".format(
4100                            "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."),
4101                            bond[1]["payCurrency"],
4102                        ),
4103                        bond[1]["couponType"],
4104                        bond[1]["couponPeriod"],
4105                        bond[1]["fixDate"].split("T")[0],
4106                    )
4107                )
4108
4109                if i < len(calendar.values) - 1:
4110                    curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4111                    nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4112                    newMonth = False if curDate.month == nextDate.month else True
4113
4114                else:
4115                    newMonth = False
4116
4117            infoText += "".join(info)
4118
4119            if show:
4120                uLogger.info("{}".format(infoText))
4121
4122            if self.calendarFile is not None:
4123                with open(self.calendarFile, "w", encoding="UTF-8") as fH:
4124                    fH.write(infoText)
4125
4126                uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile)))
4127
4128        else:
4129            infoText += "No data\n"
4130
4131        return infoText
4132
4133    def OverviewAccounts(self, show: bool = False) -> dict:
4134        """
4135        Method for parsing and show simple table with all available user accounts.
4136
4137        See also: `RequestAccounts()` and `OverviewUserInfo()` methods.
4138
4139        :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log.
4140        :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict:
4141                 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...},
4142                          "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1",
4143                                                        "status": "Opened and active account", "opened": "2018-05-23 00:00:00",
4144                                                        "closed": "—", "access": "Full access" }, ...}}`
4145        """
4146        rawAccounts = self.RequestAccounts()  # Raw responses with accounts
4147
4148        # This is an array of dict with user accounts, its `accountId`s and some parsed data:
4149        accounts = {
4150            item["id"]: {
4151                "type": TKS_ACCOUNT_TYPES[item["type"]],
4152                "name": item["name"],
4153                "status": TKS_ACCOUNT_STATUSES[item["status"]],
4154                "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4155                "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—",
4156                "access": TKS_ACCESS_LEVELS[item["accessLevel"]],
4157            } for item in rawAccounts["accounts"]
4158        }
4159
4160        # Raw and parsed data with some fields replaced in "stat" section:
4161        view = {
4162            "rawAccounts": rawAccounts,
4163            "stat": accounts,
4164        }
4165
4166        # --- Prepare simple text table with only accounts data in human-readable format:
4167        if show:
4168            info = [
4169                "# User accounts\n\n",
4170                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4171                "| Account ID   | Type                      | Status                    | Name                           |\n",
4172                "|--------------|---------------------------|---------------------------|--------------------------------|\n",
4173            ]
4174
4175            for account in view["stat"].keys():
4176                info.extend([
4177                    "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format(
4178                        account,
4179                        view["stat"][account]["type"],
4180                        view["stat"][account]["status"],
4181                        view["stat"][account]["name"],
4182                    )
4183                ])
4184
4185            infoText = "".join(info)
4186
4187            uLogger.info(infoText)
4188
4189            if self.userAccountsFile:
4190                with open(self.userAccountsFile, "w", encoding="UTF-8") as fH:
4191                    fH.write(infoText)
4192
4193                uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile)))
4194
4195        return view
4196
4197    def OverviewUserInfo(self, show: bool = False) -> dict:
4198        """
4199        Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit).
4200
4201        See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods.
4202
4203        :param show: if `False` then only dictionary returns, if `True` then also print user's data to log.
4204        :return: dict with raw parsed data from server and some calculated statistics about it.
4205        """
4206        rawUserInfo = self.RequestUserInfo()  # Raw response with common user info
4207        overviewAccount = self.OverviewAccounts(show=False)  # Raw and parsed accounts data
4208        rawAccounts = overviewAccount["rawAccounts"]  # Raw response with user accounts data
4209        accounts = overviewAccount["stat"]  # Dict with only statistics about user accounts
4210        rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()}  # Raw response with margin calculation for every account ID
4211        rawTariffLimits = self.RequestTariffLimits()  # Raw response with limits of current tariff
4212
4213        # This is dict with parsed common user data:
4214        userInfo = {
4215            "premium": "Yes" if rawUserInfo["premStatus"] else "No",
4216            "qualified": "Yes" if rawUserInfo["qualStatus"] else "No",
4217            "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]],
4218            "tariff": rawUserInfo["tariff"],
4219        }
4220
4221        # This is an array of dict with parsed margin statuses for every account IDs:
4222        margins = {}
4223        for accountId in accounts.keys():
4224            if rawMargins[accountId]:
4225                margins[accountId] = {
4226                    "currency": rawMargins[accountId]["liquidPortfolio"]["currency"],
4227                    "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]),
4228                    "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]),
4229                    "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]),
4230                    "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]),
4231                    "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]),
4232                }
4233
4234            else:
4235                margins[accountId] = {}  # Server response: margin status is disabled for current accountId
4236
4237        unary = {}  # unary-connection limits
4238        for item in rawTariffLimits["unaryLimits"]:
4239            if item["limitPerMinute"] in unary.keys():
4240                unary[item["limitPerMinute"]].extend(item["methods"])
4241
4242            else:
4243                unary[item["limitPerMinute"]] = item["methods"]
4244
4245        stream = {}  # stream-connection limits
4246        for item in rawTariffLimits["streamLimits"]:
4247            if item["limit"] in stream.keys():
4248                stream[item["limit"]].extend(item["streams"])
4249
4250            else:
4251                stream[item["limit"]] = item["streams"]
4252
4253        # This is dict with parsed limits of current tariff (connections, API methods etc.):
4254        limits = {
4255            "unary": unary,
4256            "stream": stream,
4257        }
4258
4259        # Raw and parsed data as an output result:
4260        view = {
4261            "rawUserInfo": rawUserInfo,
4262            "rawAccounts": rawAccounts,
4263            "rawMargins": rawMargins,
4264            "rawTariffLimits": rawTariffLimits,
4265            "stat": {
4266                "userInfo": userInfo,
4267                "accounts": accounts,
4268                "margins": margins,
4269                "limits": limits,
4270            },
4271        }
4272
4273        # --- Prepare text table with user information in human-readable format:
4274        if show:
4275            info = [
4276                "# Full user information\n\n",
4277                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4278                "## Common information\n\n",
4279                "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]),
4280                "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]),
4281                "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]),
4282                "* **Allowed to work with instruments:**\n{}\n".format("".join(["  - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])),
4283                "\n## User accounts\n\n",
4284            ]
4285
4286            for account in view["stat"]["accounts"].keys():
4287                info.extend([
4288                    "### ID: [{}]\n\n".format(account),
4289                    "| Parameters           | Values                                                       |\n",
4290                    "|----------------------|--------------------------------------------------------------|\n",
4291                    "| Account type:        | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]),
4292                    "| Account name:        | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]),
4293                    "| Account status:      | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]),
4294                    "| Access level:        | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]),
4295                    "| Date opened:         | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]),
4296                    "| Date closed:         | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]),
4297                ])
4298
4299                if margins[account]:
4300                    info.extend([
4301                        "| Margin status:       | Enabled                                                      |\n",
4302                        "| - Liquid portfolio:  | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])),
4303                        "| - Margin starting:   | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])),
4304                        "| - Margin minimum:    | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])),
4305                        "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)),
4306                        "| - Missing funds:     | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])),
4307                    ])
4308
4309                else:
4310                    info.append("| Margin status:       | Disabled                                                     |\n\n")
4311
4312            info.extend([
4313                "\n## Current user tariff limits\n",
4314                "\nSee also:\n",
4315                "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n",
4316                "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n",
4317                "  - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n",
4318                "  - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n",
4319                "\n### Unary limits\n",
4320            ])
4321
4322            if unary:
4323                for key, values in sorted(unary.items()):
4324                    info.append("\n* Max requests per minute: {}\n".format(key))
4325
4326                    for value in values:
4327                        info.append("  - {}\n".format(value))
4328
4329            else:
4330                info.append("\nNot available\n")
4331
4332            info.append("\n### Stream limits\n")
4333
4334            if stream:
4335                for key, values in sorted(stream.items()):
4336                    info.append("\n* Max stream connections: {}\n".format(key))
4337
4338                    for value in values:
4339                        info.append("  - {}\n".format(value))
4340
4341            else:
4342                info.append("\nNot available\n")
4343
4344            infoText = "".join(info)
4345
4346            uLogger.info(infoText)
4347
4348            if self.userInfoFile:
4349                with open(self.userInfoFile, "w", encoding="UTF-8") as fH:
4350                    fH.write(infoText)
4351
4352                uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile)))
4353
4354        return view
4355
4356
4357class Args:
4358    """
4359    If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object.
4360    """
4361    def __init__(self, **kwargs):
4362        self.__dict__.update(kwargs)
4363
4364    def __getattr__(self, item):
4365        return None
4366
4367
4368def ParseArgs():
4369    """
4370    Function get and parse command line keys.
4371
4372    See examples:
4373    - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md
4374    - in russian: https://tim55667757.github.io/TKSBrokerAPI/
4375    """
4376    parser = ArgumentParser()  # command-line string parser
4377
4378    parser.description = "TKSBrokerAPI is a python API to work with some methods of Tinkoff Open API using REST protocol. It can view history, orders and market information. Also, you can open orders and trades. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples"
4379    parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]"
4380
4381    # --- options:
4382
4383    parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the program. `False` by default.")
4384    parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/")
4385    parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.")
4386
4387    parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.")
4388    parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).")
4389
4390    parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.")
4391    parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.")
4392
4393    parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.")
4394
4395    parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.")
4396    parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.")
4397    parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.")
4398
4399    parser.add_argument("--debug-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.")
4400
4401    # --- commands:
4402
4403    parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.")
4404
4405    parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.")
4406    parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.")
4407    parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider pandas dataframe with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4408    parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.")
4409    parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!")
4410    parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4411    parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!")
4412    parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.")
4413
4414    parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.")
4415    parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.")
4416    parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.")
4417    parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.")
4418    parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.")
4419
4420    parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.")
4421    parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.")
4422    parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.")
4423    parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).")
4424
4425    parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.")
4426    parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4427    parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4428
4429    parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.")
4430    parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!")
4431    parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!")
4432    parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4433    parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4434    # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4435    # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4436
4437    parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4438    parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4439    parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` key, including for currencies tickers.")
4440    parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers, including for currencies tickers.")
4441    parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations.")
4442
4443    parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.")
4444    parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.")
4445    parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.")
4446
4447    cmdArgs = parser.parse_args()
4448    return cmdArgs
4449
4450
4451def Main(**kwargs):
4452    """
4453    Main function for work with Tinkoff Open API service. It realizes simple logic: get a lot of options and execute one command.
4454
4455    See examples:
4456    - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md
4457    - in russian: https://tim55667757.github.io/TKSBrokerAPI/
4458    """
4459    args = Args(**kwargs) if kwargs else ParseArgs()  # get and parse command-line parameters or use **kwarg parameters
4460
4461    if args.debug_level:
4462        uLogger.level = 10  # always debug level by default
4463        uLogger.handlers[0].level = args.debug_level  # level for STDOUT
4464
4465    exitCode = 0
4466    start = datetime.now(tzutc())
4467    uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format(
4468        start.strftime(TKS_PRINT_DATE_TIME_FORMAT),
4469        start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4470    ))
4471
4472    # trying to calculate full current version:
4473    buildVersion = __version__
4474    try:
4475        v = version("tksbrokerapi")
4476        buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0"  # set version as major.minor.dev0 if run as local build or local script
4477
4478    except Exception:
4479        buildVersion = __version__ + ".dev0"  # if an errors occurred then also set version as major.minor.dev0
4480
4481    uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion))
4482    uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT))
4483
4484    try:
4485        if args.version:
4486            print("TKSBrokerAPI {}".format(buildVersion))
4487            uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion))
4488
4489        else:
4490            # Init class for trading with Tinkoff Broker: TODO: rename `server` to `trader`
4491            server = TinkoffBrokerServer(
4492                token=args.token,
4493                accountId=args.account_id,
4494                useCache=not args.no_cache,
4495            )
4496
4497            # --- set some options:
4498
4499            if args.ticker:
4500                if args.ticker in server.aliasesKeys:
4501                    server.ticker = server.aliases[args.ticker]  # Replace some tickers with its aliases
4502
4503                else:
4504                    server.ticker = args.ticker
4505
4506            if args.figi:
4507                server.figi = args.figi
4508
4509            if args.depth is not None:
4510                server.depth = args.depth
4511
4512            # --- do one of commands:
4513
4514            if args.list:
4515                if args.output is not None:
4516                    server.instrumentsFile = args.output
4517
4518                server.ShowInstrumentsInfo(show=True)
4519
4520            elif args.list_xlsx:
4521                server.DumpInstrumentsAsXLSX(forceUpdate=False)
4522
4523            elif args.bonds_xlsx is not None:
4524                if args.output is not None:
4525                    server.bondsXLSXFile = args.output
4526
4527                if len(args.bonds_xlsx) == 0:
4528                    server.ExtendBondsData(instruments=server.iList["Bonds"].keys(), xlsx=True)  # request bonds with all available tickers
4529
4530                else:
4531                    server.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True)  # request list of given bonds
4532
4533            elif args.search:
4534                if args.output is not None:
4535                    server.searchResultsFile = args.output
4536
4537                server.SearchInstruments(pattern=args.search[0], show=True)
4538
4539            elif args.info:
4540                if not (args.ticker or args.figi):
4541                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4542                    raise Exception("Ticker or FIGI required")
4543
4544                if args.output is not None:
4545                    server.infoFile = args.output
4546
4547                if args.ticker:
4548                    server.SearchByTicker(requestPrice=True, show=True, debug=False)  # show info and current prices by ticker name
4549
4550                else:
4551                    server.SearchByFIGI(requestPrice=True, show=True, debug=False)  # show info and current prices by FIGI id
4552
4553            elif args.calendar is not None:
4554                if args.output is not None:
4555                    server.calendarFile = args.output
4556
4557                if len(args.calendar) == 0:
4558                    bondsData = server.ExtendBondsData(instruments=server.iList["Bonds"].keys(), xlsx=False)  # request bonds with all available tickers
4559
4560                else:
4561                    bondsData = server.ExtendBondsData(instruments=args.calendar, xlsx=False)  # request list of given bonds
4562
4563                server.ShowBondsCalendar(extBonds=bondsData, show=True)  # shows bonds payment calendar only
4564
4565            elif args.price:
4566                if not (args.ticker or args.figi):
4567                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4568                    raise Exception("Ticker or FIGI required")
4569
4570                server.GetCurrentPrices(show=True)
4571
4572            elif args.prices is not None:
4573                if args.output is not None:
4574                    server.pricesFile = args.output
4575
4576                server.GetListOfPrices(instruments=args.prices, show=True)  # WARNING: too long wait for a lot of instruments prices
4577
4578            elif args.overview:
4579                if args.output is not None:
4580                    server.overviewFile = args.output
4581
4582                server.Overview(show=True, details="full")
4583
4584            elif args.overview_digest:
4585                if args.output is not None:
4586                    server.overviewDigestFile = args.output
4587
4588                server.Overview(show=True, details="digest")
4589
4590            elif args.overview_positions:
4591                if args.output is not None:
4592                    server.overviewPositionsFile = args.output
4593
4594                server.Overview(show=True, details="positions")
4595
4596            elif args.overview_orders:
4597                if args.output is not None:
4598                    server.overviewOrdersFile = args.output
4599
4600                server.Overview(show=True, details="orders")
4601
4602            elif args.overview_analytics:
4603                if args.output is not None:
4604                    server.overviewAnalyticsFile = args.output
4605
4606                server.Overview(show=True, details="analytics")
4607
4608            elif args.deals is not None:
4609                if args.output is not None:
4610                    server.reportFile = args.output
4611
4612                if 0 <= len(args.deals) < 3:
4613                    server.Deals(
4614                        start=args.deals[0] if len(args.deals) >= 1 else None,
4615                        end=args.deals[1] if len(args.deals) == 2 else None,
4616                        show=True,  # Always show deals report in console
4617                        showCancelled=not args.no_cancelled,  # If --no-cancelled key then remove cancelled operations from the deals report. False by default.
4618                    )
4619
4620                else:
4621                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
4622                    raise Exception("Incorrect value")
4623
4624            elif args.history is not None:
4625                if args.output is not None:
4626                    server.historyFile = args.output
4627
4628                if 0 <= len(args.history) < 3:
4629                    dataReceived = server.History(
4630                        start=args.history[0] if len(args.history) >= 1 else None,
4631                        end=args.history[1] if len(args.history) == 2 else None,
4632                        interval="hour" if args.interval is None or not args.interval else args.interval,
4633                        onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing,
4634                        csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep,
4635                        show=True,  # shows all downloaded candles in console
4636                    )
4637
4638                    if args.render_chart is not None and dataReceived is not None:
4639                        iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
4640
4641                        server.ShowHistoryChart(
4642                            candles=dataReceived,
4643                            interact=iChart,
4644                            openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
4645                        )
4646
4647                else:
4648                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
4649                    raise Exception("Incorrect value")
4650
4651            elif args.load_history is not None:
4652                histData = server.LoadHistory(filePath=args.load_history)  # load data from file and show history in console
4653
4654                if args.render_chart is not None and histData is not None:
4655                    iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
4656                    server.ticker = os.path.basename(args.load_history)  # use filename as ticker name for PriceGenerator's chart
4657
4658                    server.ShowHistoryChart(
4659                        candles=histData,
4660                        interact=iChart,
4661                        openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
4662                    )
4663
4664            elif args.trade is not None:
4665                if 1 <= len(args.trade) <= 5:
4666                    server.Trade(
4667                        operation=args.trade[0],
4668                        lots=int(args.trade[1]) if len(args.trade) >= 2 else 1,
4669                        tp=float(args.trade[2]) if len(args.trade) >= 3 else 0.,
4670                        sl=float(args.trade[3]) if len(args.trade) >= 4 else 0.,
4671                        expDate=args.trade[4] if len(args.trade) == 5 else "Undefined",
4672                    )
4673
4674                else:
4675                    uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4676
4677            elif args.buy is not None:
4678                if 0 <= len(args.buy) <= 4:
4679                    server.Buy(
4680                        lots=int(args.buy[0]) if len(args.buy) >= 1 else 1,
4681                        tp=float(args.buy[1]) if len(args.buy) >= 2 else 0.,
4682                        sl=float(args.buy[2]) if len(args.buy) >= 3 else 0.,
4683                        expDate=args.buy[3] if len(args.buy) == 4 else "Undefined",
4684                    )
4685
4686                else:
4687                    uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4688
4689            elif args.sell is not None:
4690                if 0 <= len(args.sell) <= 4:
4691                    server.Sell(
4692                        lots=int(args.sell[0]) if len(args.sell) >= 1 else 1,
4693                        tp=float(args.sell[1]) if len(args.sell) >= 2 else 0.,
4694                        sl=float(args.sell[2]) if len(args.sell) >= 3 else 0.,
4695                        expDate=args.sell[3] if len(args.sell) == 4 else "Undefined",
4696                    )
4697
4698                else:
4699                    uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4700
4701            elif args.order:
4702                if 4 <= len(args.order) <= 7:
4703                    server.Order(
4704                        operation=args.order[0],
4705                        orderType=args.order[1],
4706                        lots=int(args.order[2]),
4707                        targetPrice=float(args.order[3]),
4708                        limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0.,
4709                        stopType=args.order[5] if len(args.order) >= 6 else "Limit",
4710                        expDate=args.order[6] if len(args.order) == 7 else "Undefined",
4711                    )
4712
4713                else:
4714                    uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`")
4715
4716            elif args.buy_limit:
4717                server.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1])
4718
4719            elif args.sell_limit:
4720                server.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1])
4721
4722            elif args.buy_stop:
4723                if 2 <= len(args.buy_stop) <= 7:
4724                    server.BuyStop(
4725                        lots=int(args.buy_stop[0]),
4726                        targetPrice=float(args.buy_stop[1]),
4727                        limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0.,
4728                        stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit",
4729                        expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined",
4730                    )
4731
4732                else:
4733                    uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4734
4735            elif args.sell_stop:
4736                if 2 <= len(args.sell_stop) <= 7:
4737                    server.SellStop(
4738                        lots=int(args.sell_stop[0]),
4739                        targetPrice=float(args.sell_stop[1]),
4740                        limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0.,
4741                        stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit",
4742                        expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined",
4743                    )
4744
4745                else:
4746                    uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help")
4747
4748            # elif args.buy_order_grid is not None:
4749            #     # update order grid work with api v2
4750            #     if len(args.buy_order_grid) == 2:
4751            #         orderParams = server.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid))
4752            #
4753            #         for order in orderParams:
4754            #             server.Order(operation="Buy", lots=order["lot"], price=order["price"])
4755            #
4756            #     else:
4757            #         uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
4758            #
4759            # elif args.sell_order_grid is not None:
4760            #     # update order grid work with api v2
4761            #     if len(args.sell_order_grid) >= 2:
4762            #         orderParams = server.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid))
4763            #
4764            #         for order in orderParams:
4765            #             server.Order(operation="Sell", lots=order["lot"], price=order["price"])
4766            #
4767            #     else:
4768            #         uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
4769
4770            elif args.close_order is not None:
4771                server.CloseOrders(args.close_order)  # close only one order
4772
4773            elif args.close_orders is not None:
4774                server.CloseOrders(args.close_orders)  # close list of orders
4775
4776            elif args.close_trade:
4777                if not args.ticker:
4778                    uLogger.error("`--ticker` key is required for this operation!")
4779                    raise Exception("Ticker required")
4780
4781                server.CloseTrades([args.ticker])  # close only one trade
4782
4783            elif args.close_trades is not None:
4784                server.CloseTrades(args.close_trades)  # close trades for list of tickers
4785
4786            elif args.close_all is not None:
4787                server.CloseAll(*args.close_all)
4788
4789            elif args.limits:
4790                if args.output is not None:
4791                    server.withdrawalLimitsFile = args.output
4792
4793                server.OverviewLimits(show=True)
4794
4795            elif args.user_info:
4796                if args.output is not None:
4797                    server.userInfoFile = args.output
4798
4799                server.OverviewUserInfo(show=True)
4800
4801            elif args.account:
4802                if args.output is not None:
4803                    server.userAccountsFile = args.output
4804
4805                server.OverviewAccounts(show=True)
4806
4807            else:
4808                uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.")
4809                raise Exception("There is no command to execute")
4810
4811    except Exception:
4812        trace = tb.format_exc()
4813        for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]:
4814            if e in trace:
4815                uLogger.error("Check your Internet connection! Failed to establish connection to broker server!")
4816                break
4817
4818        uLogger.debug(trace)
4819        uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues")
4820        exitCode = 255  # an error occurred, must be open a ticket for this issue
4821
4822    finally:
4823        finish = datetime.now(tzutc())
4824
4825        if exitCode == 0:
4826            uLogger.debug("All operations were finished success (summary code is 0).")
4827
4828        else:
4829            uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format(
4830                os.path.abspath(uLog.defaultLogFile), exitCode,
4831            ))
4832
4833        uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start))
4834        uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format(
4835            finish.strftime(TKS_PRINT_DATE_TIME_FORMAT),
4836            finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4837        ))
4838
4839        if not kwargs:
4840            sys.exit(exitCode)
4841
4842        else:
4843            return exitCode
4844
4845
4846if __name__ == "__main__":
4847    Main()
def NanoToFloat(units: str, nano: int) -> float:
78def NanoToFloat(units: str, nano: int) -> float:
79    """
80    Convert number in nano-view mode with string parameter `units` and integer parameter `nano` to float view. Examples:
81
82    `NanoToFloat(units="2", nano=500000000) -> 2.5`
83
84    `NanoToFloat(units="0", nano=50000000) -> 0.05`
85
86    :param units: integer string or integer parameter that represents the integer part of number
87    :param nano: integer string or integer parameter that represents the fractional part of number
88    :return: float view of number
89    """
90    return int(units) + int(nano) * NANO

Convert number in nano-view mode with string parameter units and integer parameter nano to float view. Examples:

NanoToFloat(units="2", nano=500000000) -> 2.5

NanoToFloat(units="0", nano=50000000) -> 0.05

Parameters
  • units: integer string or integer parameter that represents the integer part of number
  • nano: integer string or integer parameter that represents the fractional part of number
Returns

float view of number

def FloatToNano(number: float) -> dict:
 93def FloatToNano(number: float) -> dict:
 94    """
 95    Convert float number to nano-type view: dictionary with string `units` and integer `nano` parameters `{"units": "string", "nano": integer}`. Examples:
 96
 97    `FloatToNano(number=2.5) -> {"units": "2", "nano": 500000000}`
 98
 99    `FloatToNano(number=0.05) -> {"units": "0", "nano": 50000000}`
100
101    :param number: float number
102    :return: nano-type view of number: `{"units": "string", "nano": integer}`
103    """
104    splitByPoint = str(number).split(".")
105    frac = 0
106
107    if len(splitByPoint) > 1:
108        if len(splitByPoint[1]) <= 9:
109            frac = int("{}{}".format(
110                int(splitByPoint[1]),
111                "0" * (9 - len(splitByPoint[1])),
112            ))
113
114    if (number < 0) and (frac > 0):
115        frac = -frac
116
117    return {"units": str(int(number)), "nano": frac}

Convert float number to nano-type view: dictionary with string units and integer nano parameters {"units": "string", "nano": integer}. Examples:

FloatToNano(number=2.5) -> {"units": "2", "nano": 500000000}

FloatToNano(number=0.05) -> {"units": "0", "nano": 50000000}

Parameters
  • number: float number
Returns

nano-type view of number: {"units": "string", "nano": integer}

def GetDatesAsString(start: str = None, end: str = None) -> tuple:
120def GetDatesAsString(start: str = None, end: str = None) -> tuple:
121    """
122    Create tuple of date and time strings with timezone parsed from user-friendly date.
123
124    User dates format must be like: `%Y-%m-%d`, e.g. `2020-02-03` (3 Feb, 2020).
125
126    Example input: "2022-06-01" "2022-06-20" -> output: ("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z")
127    An error exception will occur if input date has incorrect format.
128
129    If `start=None`, `end=None` then return dates from yesterday to the end of the day.
130    If `start=some_date_1`, `end=None` then return dates from `some_date_1` to the end of the day.
131    If `start=some_date_1`, `end=some_date_2` then return dates from start of `some_date_1` to end of `some_date_2`.
132    Start day may be negative integer numbers: `-1`, `-2`, `-3` - how many days ago.
133
134    Also, you can use keywords for start if `end=None`:
135    `today` (from 00:00:00 to the end of current day),
136    `yesterday` (-1 day from 00:00:00 to 23:59:59),
137    `week` (-7 day from 00:00:00 to the end of current day),
138    `month` (-30 day from 00:00:00 to the end of current day),
139    `year` (-365 day from 00:00:00 to the end of current day),
140
141    :return: tuple with 2 strings `(start, end)` dates in UTC ISO time format `%Y-%m-%dT%H:%M:%SZ` for OpenAPI.
142             See date and time format here: `TKSEnums.TKS_DATE_TIME_FORMAT`.
143             Example: `("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z")`. Second string is the end of the last day.
144    """
145    uLogger.debug("Input start day is [{}] (UTC), end day is [{}] (UTC)".format(start, end))
146    s = datetime.now(tzutc()).replace(hour=0, minute=0, second=0, microsecond=0)  # start of the current day
147    e = s.replace(hour=23, minute=59, second=59, microsecond=0)  # end of the current day
148
149    # time between start and the end of the current day:
150    if start is None or start.lower() == "today":
151        pass
152
153    # from start of the last day to the end of the last day:
154    elif start.lower() == "yesterday":
155        s -= timedelta(days=1)
156        e -= timedelta(days=1)
157
158    # week (-7 day from 00:00:00 to the end of the current day):
159    elif start.lower() == "week":
160        s -= timedelta(days=6)  # +1 current day already taken into account
161
162    # month (-30 day from 00:00:00 to the end of current day):
163    elif start.lower() == "month":
164        s -= timedelta(days=29)  # +1 current day already taken into account
165
166    # year (-365 day from 00:00:00 to the end of current day):
167    elif start.lower() == "year":
168        s -= timedelta(days=364)  # +1 current day already taken into account
169
170    # -N days ago to the end of current day:
171    elif start.startswith('-') and start[1:].isdigit():
172        s -= timedelta(days=abs(int(start)) - 1)  # +1 current day already taken into account
173
174    # dates between start day at 00:00:00 and the end of the last day at 23:59:59:
175    else:
176        s = datetime.strptime(start, "%Y-%m-%d").replace(hour=0, minute=0, second=0, microsecond=0, tzinfo=tzutc())
177        e = datetime.strptime(end, "%Y-%m-%d").replace(hour=23, minute=59, second=59, microsecond=0, tzinfo=tzutc()) if end is not None else e
178
179    # converting to UTC ISO time formatted with Z suffix for Tinkoff Open API:
180    s = s.strftime(TKS_DATE_TIME_FORMAT)
181    e = e.strftime(TKS_DATE_TIME_FORMAT)
182
183    uLogger.debug("Start day converted to UTC ISO format, with Z: [{}], and the end day: [{}]".format(s, e))
184
185    return s, e

Create tuple of date and time strings with timezone parsed from user-friendly date.

User dates format must be like: %Y-%m-%d, e.g. 2020-02-03 (3 Feb, 2020).

Example input: "2022-06-01" "2022-06-20" -> output: ("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z") An error exception will occur if input date has incorrect format.

If start=None, end=None then return dates from yesterday to the end of the day. If start=some_date_1, end=None then return dates from some_date_1 to the end of the day. If start=some_date_1, end=some_date_2 then return dates from start of some_date_1 to end of some_date_2. Start day may be negative integer numbers: -1, -2, -3 - how many days ago.

Also, you can use keywords for start if end=None: today (from 00:00:00 to the end of current day), yesterday (-1 day from 00:00:00 to 23:59:59), week (-7 day from 00:00:00 to the end of current day), month (-30 day from 00:00:00 to the end of current day), year (-365 day from 00:00:00 to the end of current day),

Returns

tuple with 2 strings (start, end) dates in UTC ISO time format %Y-%m-%dT%H:%M:%SZ for OpenAPI. See date and time format here: TKSEnums.TKS_DATE_TIME_FORMAT. Example: ("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z"). Second string is the end of the last day.

class TinkoffBrokerServer:
 188class TinkoffBrokerServer:
 189    """
 190    This class implements methods to work with Tinkoff broker server.
 191
 192    Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/
 193
 194    About `token`: https://tinkoff.github.io/investAPI/token/
 195    """
 196    def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None:
 197        """
 198        Main class init.
 199
 200        :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`.
 201        :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
 202                          Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.
 203        :param useCache: use default cache file with raw data to use instead of `iList`.
 204                         True by default. Cache is auto-update if new day has come.
 205                         If you don't want to use cache and always updates raw data then set `useCache=False`.
 206        :param defaultCache: path to default cache file. `dump.json` by default.
 207        """
 208        if token is None or not token:
 209            try:
 210                self.token = r"{}".format(os.environ["TKS_API_TOKEN"])
 211                uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/")
 212
 213            except KeyError:
 214                uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/")
 215                raise Exception("Token required")
 216
 217        else:
 218            self.token = token  # highly priority than environment variable 'TKS_API_TOKEN'
 219            uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`")
 220
 221        if accountId is None or not accountId:
 222            try:
 223                self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"])
 224                uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId))
 225
 226            except KeyError:
 227                uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).")
 228
 229        else:
 230            self.accountId = accountId  # highly priority than environment variable 'TKS_ACCOUNT_ID'
 231            uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId))
 232
 233        self.version = __version__  # duplicate here used TKSBrokerAPI main version
 234        """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
 235
 236        Latest version: https://pypi.org/project/tksbrokerapi/
 237        """
 238
 239        self.aliases = TKS_TICKER_ALIASES
 240        """Some aliases instead official tickers.
 241
 242        See also: `TKSEnums.TKS_TICKER_ALIASES`
 243        """
 244
 245        self.aliasesKeys = self.aliases.keys()  # re-calc only first time at class init
 246
 247        self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED  # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there
 248
 249        self.ticker = ""
 250        """String with ticker, e.g. `GOOGL`. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 251
 252        See also: `SearchByTicker()`, `SearchInstruments()`.
 253        """
 254
 255        self.figi = ""
 256        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`.
 257
 258        See also: `SearchByFIGI()`, `SearchInstruments()`.
 259        """
 260
 261        self.depth = 1
 262        """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI.
 263
 264        See also: `GetCurrentPrices()`.
 265        """
 266
 267        self.server = r"https://invest-public-api.tinkoff.ru/rest"
 268        """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
 269
 270        See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`.
 271        """
 272
 273        uLogger.debug("Broker API server: {}".format(self.server))
 274
 275        self.timeout = 15
 276        """Server operations timeout in seconds. Default: `15`.
 277
 278        See also: `SendAPIRequest()`.
 279        """
 280
 281        self.headers = {
 282            "Content-Type": "application/json",
 283            "accept": "application/json",
 284            "Authorization": "Bearer {}".format(self.token),
 285            "x-app-name": "Tim55667757.TKSBrokerAPI",
 286        }
 287        """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`.
 288
 289        See also: `SendAPIRequest()`.
 290        """
 291
 292        self.body = None
 293        """Request body which send to broker server. Default: `None`.
 294
 295        See also: `SendAPIRequest()`.
 296        """
 297
 298        self.historyFile = None
 299        """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only pandas dataframe.
 300
 301        See also: `History()`.
 302        """
 303
 304        self.htmlHistoryFile = "index.html"
 305        """Full path to the html file where rendered candles chart stored. Default: `index.html`.
 306
 307        See also: `ShowHistoryChart()`.
 308        """
 309
 310        self.instrumentsFile = "instruments.md"
 311        """Filename where full available to user instruments list will be saved. Default: `instruments.md`.
 312
 313        See also: `ShowInstrumentsInfo()`.
 314        """
 315
 316        self.searchResultsFile = "search-results.md"
 317        """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`.
 318
 319        See also: `SearchInstruments()`.
 320        """
 321
 322        self.pricesFile = "prices.md"
 323        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 324
 325        See also: `GetListOfPrices()`.
 326        """
 327
 328        self.infoFile = "info.md"
 329        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 330
 331        See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`.
 332        """
 333
 334        self.bondsXLSXFile = "ext-bonds.xlsx"
 335        """Filename where wider pandas dataframe with more information about bonds: main info, current prices, 
 336        bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`.
 337
 338        See also: `ExtendBondsData()`.
 339        """
 340
 341        self.calendarFile = "calendar.md"
 342        """Filename where bonds payment calendar will be saved. Default: `calendar.md`.
 343        
 344        Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`.
 345
 346        See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`.
 347        """
 348
 349        self.overviewFile = "overview.md"
 350        """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`.
 351
 352        See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`.
 353        """
 354
 355        self.overviewDigestFile = "overview-digest.md"
 356        """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`.
 357
 358        See also: `Overview()` with parameter `details="digest"`.
 359        """
 360
 361        self.overviewPositionsFile = "overview-positions.md"
 362        """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`.
 363
 364        See also: `Overview()` with parameter `details="positions"`.
 365        """
 366
 367        self.overviewOrdersFile = "overview-orders.md"
 368        """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`.
 369
 370        See also: `Overview()` with parameter `details="orders"`.
 371        """
 372
 373        self.overviewAnalyticsFile = "overview-analytics.md"
 374        """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`.
 375
 376        See also: `Overview()` with parameter `details="analytics"`.
 377        """
 378
 379        self.reportFile = "deals.md"
 380        """Filename where history of deals and trade statistics will be saved. Default: `deals.md`.
 381
 382        See also: `Deals()`.
 383        """
 384
 385        self.withdrawalLimitsFile = "limits.md"
 386        """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`.
 387
 388        See also: `OverviewLimits()` and `RequestLimits()`.
 389        """
 390
 391        self.userInfoFile = "user-info.md"
 392        """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`.
 393
 394        See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`.
 395        """
 396
 397        self.userAccountsFile = "accounts.md"
 398        """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`.
 399
 400        See also: `OverviewAccounts()`, `RequestAccounts()`.
 401        """
 402
 403        self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache
 404        """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`.
 405
 406        Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`.
 407
 408        See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`.
 409        """
 410
 411        self.iList = None  # init iList for raw instruments data
 412        """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`.
 413        
 414        See also: `Listing()`, `DumpInstruments()`.
 415        """
 416
 417        # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server:
 418        if useCache:
 419            if os.path.exists(self.iListDumpFile):
 420                dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc())  # dump modification date and time
 421                curTime = datetime.now(tzutc())
 422
 423                if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year):
 424                    uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
 425
 426                    self.DumpInstruments(forceUpdate=True)  # updating self.iList and dump file
 427
 428                else:
 429                    self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8"))  # load iList from dump
 430
 431                    uLogger.debug("Local cache with raw instruments data is used: [{}]".format(os.path.abspath(self.iListDumpFile)))
 432                    uLogger.debug("Dump file was last modified [{}] UTC".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
 433
 434            else:
 435                uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...")
 436                self.DumpInstruments(forceUpdate=True)  # updating self.iList and creating default dump file
 437
 438        else:
 439            self.iList = self.Listing()  # request new raw instruments data from broker server
 440            self.DumpInstruments(forceUpdate=False)  # save raw instrument's data to default dump file `iListDumpFile`
 441
 442        self.priceModel = PriceGenerator()  # init PriceGenerator object to work with candles data
 443        """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
 444
 445        See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
 446        """
 447
 448    @staticmethod
 449    def _ParseJSON(rawData="{}", debug: bool = False) -> dict:
 450        """
 451        Parse JSON from response string.
 452
 453        :param rawData: this is a string with JSON-formatted text.
 454        :param debug: if `True` then print more debug information.
 455        :return: JSON (dictionary), parsed from server response string.
 456        """
 457        if debug:
 458            uLogger.debug("Raw text body:")
 459            uLogger.debug(rawData)
 460
 461        responseJSON = json.loads(rawData) if rawData else {}
 462
 463        if debug:
 464            uLogger.debug("JSON formatted:")
 465            for jsonLine in json.dumps(responseJSON, indent=4).split('\n'):
 466                uLogger.debug(jsonLine)
 467
 468        return responseJSON
 469
 470    def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5, debug: bool = False) -> dict:
 471        """
 472        Send GET or POST request to broker server and receive JSON object.
 473
 474        self.header: must be defining with dictionary of headers.
 475        self.body: if define then used as request body. None by default.
 476        self.timeout: global request timeout, 15 seconds by default.
 477        :param url: url with REST request.
 478        :param reqType: send "GET" or "POST" request. "GET" by default.
 479        :param retry: how many times retry after first request if an 5xx server errors occurred.
 480        :param pause: sleep time in seconds between retries.
 481        :param debug: if `True` then print more debug information, e.g. request and response parameters, headers etc.
 482        :return: response JSON (dictionary) from broker.
 483        """
 484        if reqType not in ("GET", "POST"):
 485            uLogger.error("You can define request type: 'GET' or 'POST'!")
 486            raise Exception("Incorrect value")
 487
 488        if debug:
 489            uLogger.debug("Request parameters:")
 490            uLogger.debug("    - REST API URL: {}".format(url))
 491            uLogger.debug("    - request type: {}".format(reqType))
 492            uLogger.debug("    - headers: {}".format(str(self.headers).replace(self.token, "*** request token ***")))
 493            uLogger.debug("    - body: {}".format(self.body))
 494
 495        # fast hack to avoid all operations with some tickers/FIGI
 496        responseJSON = {}
 497        oK = True
 498        for item in self.exclude:
 499            if item in url:
 500                if debug:
 501                    uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude)))
 502
 503                oK = False
 504                break
 505
 506        if oK:
 507            counter = 0
 508            response = None
 509            errMsg = ""
 510
 511            while not response and counter <= retry:
 512                if reqType == "GET":
 513                    response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout)
 514
 515                if reqType == "POST":
 516                    response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout)
 517
 518                if debug:
 519                    uLogger.debug("Response:")
 520                    uLogger.debug("    - status code: {}".format(response.status_code))
 521                    uLogger.debug("    - reason: {}".format(response.reason))
 522                    uLogger.debug("    - body length: {}".format(len(response.text)))
 523                    uLogger.debug("    - headers: {}".format(response.headers))
 524
 525                # Server returns some headers:
 526                # - `x-ratelimit-limit` - shows the settings of the current user limit for this method.
 527                # - `x-ratelimit-remaining` - the number of remaining requests of this type per minute.
 528                # - `x-ratelimit-reset` - time in seconds before resetting the request counter.
 529                # See: https://tinkoff.github.io/investAPI/grpc/#kreya
 530                if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0":
 531                    rateLimitWait = int(response.headers["x-ratelimit-reset"])
 532                    uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait))
 533                    sleep(rateLimitWait)
 534
 535                # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
 536                if 400 <= response.status_code < 500:
 537                    msg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 538                    uLogger.debug("    - not oK, but do not retry for 4xx errors, {}".format(msg))
 539                    counter = retry + 1
 540
 541                if 500 <= response.status_code < 600:
 542                    errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 543                    uLogger.debug("    - not oK, {}".format(errMsg))
 544                    counter += 1
 545
 546                    if counter <= retry:
 547                        uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause))
 548                        sleep(pause)
 549
 550            responseJSON = self._ParseJSON(response.text)
 551
 552            if errMsg:
 553                uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/")
 554                uLogger.error("    - not oK, {}".format(errMsg))
 555
 556        return responseJSON
 557
 558    def _IUpdater(self, iType: str) -> tuple:
 559        """
 560        Request instrument by type from server. See available API methods for instruments:
 561        Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies
 562        Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares
 563        Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds
 564        Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs
 565        Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures
 566
 567        :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 568        :return: tuple with iType name and list of available instruments of current type for defined user token.
 569        """
 570        result = []
 571
 572        if iType in TKS_INSTRUMENTS:
 573            uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType))
 574
 575            # all instruments have the same body in API v2 requests:
 576            self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"})  # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL]
 577            instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType)
 578            result = self.SendAPIRequest(instrumentURL, reqType="POST", debug=False)["instruments"]
 579
 580        return iType, result
 581
 582    def _IWrapper(self, kwargs):
 583        """
 584        Wrapper runs instrument's update method `_IUpdater()`.
 585        It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206
 586        """
 587        return self._IUpdater(**kwargs)
 588
 589    def Listing(self) -> dict:
 590        """
 591        Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
 592
 593        :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
 594        """
 595        uLogger.debug("Requesting all available instruments for current account. Wait, please...")
 596        uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES))
 597
 598        # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService
 599        # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 600        iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS]
 601
 602        poolUpdater = ThreadPool(processes=CPU_USAGES)  # create pool for update instruments in parallel mode
 603        listing = poolUpdater.map(self._IWrapper, iParams)  # execute update operations
 604        poolUpdater.close()
 605
 606        # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures.
 607        # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method
 608        iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing}
 609
 610        # calculate minimum price increment (step) for all instruments and set up instrument's type:
 611        for iType in iList.keys():
 612            for ticker in iList[iType]:
 613                iList[iType][ticker]["type"] = iType
 614
 615                if "minPriceIncrement" in iList[iType][ticker].keys():
 616                    iList[iType][ticker]["step"] = NanoToFloat(
 617                        iList[iType][ticker]["minPriceIncrement"]["units"],
 618                        iList[iType][ticker]["minPriceIncrement"]["nano"],
 619                    )
 620
 621                else:
 622                    iList[iType][ticker]["step"] = 0  # hack to avoid empty value in some instruments, e.g. futures
 623
 624        return iList
 625
 626    def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
 627        """
 628        Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
 629
 630        See also: `DumpInstruments()`, `Listing()`.
 631
 632        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 633                            otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) .
 634        """
 635        if self.iListDumpFile is None or not self.iListDumpFile:
 636            uLogger.error("Output name of dump file must be defined!")
 637            raise Exception("Filename required")
 638
 639        if not self.iList or forceUpdate:
 640            self.iList = self.Listing()
 641
 642        xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx"
 643
 644        # Save as XLSX with separated sheets for every type of instruments:
 645        with pd.ExcelWriter(
 646                path=xlsxDumpFile,
 647                date_format=TKS_DATE_FORMAT,
 648                datetime_format=TKS_DATE_TIME_FORMAT,
 649                mode="w",
 650        ) as writer:
 651            for iType in TKS_INSTRUMENTS:
 652                df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index")  # generate pandas object from self.iList dictionary
 653                df = df[sorted(df)]  # sorted by column names
 654                df = df.applymap(
 655                    lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item,
 656                    na_action="ignore",
 657                )  # converting numbers from nano-type to float in every cell
 658                df.to_excel(
 659                    writer,
 660                    sheet_name=iType,
 661                    encoding="UTF-8",
 662                    freeze_panes=(1, 1),
 663                )  # saving as XLSX-file with freeze first row and column as headers
 664
 665        uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))
 666
 667    def DumpInstruments(self, forceUpdate: bool = True) -> str:
 668        """
 669        Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
 670        using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file.
 671
 672        See also: `DumpInstrumentsAsXLSX()`, `Listing()`.
 673
 674        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 675                            otherwise just saves exist `iList` as JSON-file (default: `dump.json`).
 676        :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file.
 677        """
 678        if self.iListDumpFile is None or not self.iListDumpFile:
 679            uLogger.error("Output name of dump file must be defined!")
 680            raise Exception("Filename required")
 681
 682        if not self.iList or forceUpdate:
 683            self.iList = self.Listing()
 684
 685        jsonDump = json.dumps(self.iList, indent=4, sort_keys=False)  # create JSON object as string
 686        with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH:
 687            fH.write(jsonDump)
 688
 689        uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile)))
 690
 691        return jsonDump
 692
 693    def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str:
 694        """
 695        Show information about one instrument defined by json data and prints it in Markdown format.
 696
 697        See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`.
 698
 699        :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self.ticker]`
 700        :param show: if `True` then also printing information about instrument and its current price.
 701        :return: multilines text in Markdown format with information about one instrument.
 702        """
 703        splitLine = "|                                                             |                                                        |\n"
 704        infoText = ""
 705
 706        if iJSON is not None and iJSON and isinstance(iJSON, dict):
 707            info = [
 708                "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]),
 709                "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
 710                "| Parameters                                                  | Values                                                 |\n",
 711                "|-------------------------------------------------------------|--------------------------------------------------------|\n",
 712                "| Ticker:                                                     | {:<54} |\n".format(iJSON["ticker"]),
 713                "| Full name:                                                  | {:<54} |\n".format(iJSON["name"]),
 714            ]
 715
 716            if "sector" in iJSON.keys() and iJSON["sector"]:
 717                info.append("| Sector:                                                     | {:<54} |\n".format(iJSON["sector"]))
 718
 719            info.append("| Country of instrument:                                      | {:<54} |\n".format("{}{}".format(
 720                "({}) ".format(iJSON["countryOfRisk"]) if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] else "",
 721                iJSON["countryOfRiskName"] if "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"] else "",
 722            )))
 723
 724            info.extend([
 725                splitLine,
 726                "| FIGI (Financial Instrument Global Identifier):              | {:<54} |\n".format(iJSON["figi"]),
 727                "| Exchange:                                                   | {:<54} |\n".format(iJSON["exchange"]),
 728            ])
 729
 730            if "isin" in iJSON.keys() and iJSON["isin"]:
 731                info.append("| ISIN (International Securities Identification Number):      | {:<54} |\n".format(iJSON["isin"]))
 732
 733            if "classCode" in iJSON.keys():
 734                info.append("| Class Code (exchange section where instrument is traded):   | {:<54} |\n".format(iJSON["classCode"]))
 735
 736            info.extend([
 737                splitLine,
 738                "| Current broker security trading status:                     | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]),
 739                splitLine,
 740                "| Buy operations allowed:                                     | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"),
 741                "| Sale operations allowed:                                    | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"),
 742                "| Short positions allowed:                                    | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"),
 743            ])
 744
 745            if iJSON["figi"]:
 746                self.figi = iJSON["figi"]
 747                iJSON = iJSON | self.RequestTradingStatus()
 748
 749                info.extend([
 750                    splitLine,
 751                    "| Limit orders allowed:                                       | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"),
 752                    "| Market orders allowed:                                      | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"),
 753                    "| API trade allowed:                                          | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"),
 754                ])
 755
 756            info.append(splitLine)
 757
 758            if "type" in iJSON.keys() and iJSON["type"]:
 759                info.append("| Type of the instrument:                                     | {:<54} |\n".format(iJSON["type"]))
 760
 761            if "futuresType" in iJSON.keys() and iJSON["futuresType"]:
 762                info.append("| Futures type:                                               | {:<54} |\n".format(iJSON["futuresType"]))
 763
 764            if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]:
 765                info.append("| IPO date:                                                   | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", "")))
 766
 767            if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]:
 768                info.append("| Released date:                                              | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", "")))
 769
 770            if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]:
 771                info.append("| Rebalancing frequency:                                      | {:<54} |\n".format(iJSON["rebalancingFreq"]))
 772
 773            if "focusType" in iJSON.keys() and iJSON["focusType"]:
 774                info.append("| Focusing type:                                              | {:<54} |\n".format(iJSON["focusType"]))
 775
 776            if "assetType" in iJSON.keys() and iJSON["assetType"]:
 777                info.append("| Asset type:                                                 | {:<54} |\n".format(iJSON["assetType"]))
 778
 779            if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]:
 780                info.append("| Basic asset:                                                | {:<54} |\n".format(iJSON["basicAsset"]))
 781
 782            if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]:
 783                info.append("| Basic asset size:                                           | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"]))))
 784
 785            if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]:
 786                info.append("| ISO currency name:                                          | {:<54} |\n".format(iJSON["isoCurrencyName"]))
 787
 788            if "currency" in iJSON.keys():
 789                info.append("| Payment currency:                                           | {:<54} |\n".format(iJSON["currency"]))
 790
 791            if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys():
 792                info.append("| Nominal currency:                                           | {:<54} |\n".format(iJSON["nominal"]["currency"]))
 793
 794            if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]:
 795                info.append("| First trade date:                                           | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", "")))
 796
 797            if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]:
 798                info.append("| Last trade date:                                            | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", "")))
 799
 800            if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]:
 801                info.append("| Date of expiration:                                         | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", "")))
 802
 803            if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]:
 804                info.append("| State registration date:                                    | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", "")))
 805
 806            if "placementDate" in iJSON.keys() and iJSON["placementDate"]:
 807                info.append("| Placement date:                                             | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", "")))
 808
 809            if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]:
 810                info.append("| Maturity date:                                              | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", "")))
 811
 812            if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]:
 813                info.append("| Perpetual bond:                                             | Yes                                                    |\n")
 814
 815            if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]:
 816                info.append("| Over-the-counter (OTC) securities:                          | Yes                                                    |\n")
 817
 818            iExt = None
 819            if iJSON["type"] == "Bonds":
 820                info.extend([
 821                    splitLine,
 822                    "| Bond issue (size / plan):                                   | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])),
 823                    "| Nominal price (100%):                                       | {:<54} |\n".format("{} {}".format(
 824                        "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."),
 825                        iJSON["nominal"]["currency"],
 826                    )),
 827                ])
 828
 829                if "floatingCouponFlag" in iJSON.keys():
 830                    info.append("| Floating coupon:                                            | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No"))
 831
 832                if "amortizationFlag" in iJSON.keys():
 833                    info.append("| Amortization:                                               | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No"))
 834
 835                info.append(splitLine)
 836
 837                if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]:
 838                    info.append("| Number of coupon payments per year:                         | {:<54} |\n".format(iJSON["couponQuantityPerYear"]))
 839
 840                iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False)  # extended bonds data
 841
 842                info.extend([
 843                    "| Days last to maturity date:                                 | {:<54} |\n".format(iExt["daysToMaturity"][0]),
 844                    "| Coupons yield (average coupon daily yield * 365):           | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])),
 845                    "| Current price yield (average daily yield * 365):            | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])),
 846                ])
 847
 848                if "aciValue" in iJSON.keys() and iJSON["aciValue"]:
 849                    info.append("| Current accumulated coupon income (ACI):                    | {:<54} |\n".format("{:.2f} {}".format(
 850                        NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]),
 851                        iJSON["aciValue"]["currency"]
 852                    )))
 853
 854            if "currentPrice" in iJSON.keys():
 855                info.append(splitLine)
 856
 857                currency = iJSON["currency"] if "currency" in iJSON.keys() else ""  # nominal currency for bonds, otherwise currency of instrument
 858                aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else ""  # payment currency
 859
 860                bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0  # previous close price of bond
 861                bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0  # last price of bond
 862                bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0  # max price of bond
 863                bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0  # min price of bond
 864                bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0  # delta between last deal price and last close
 865
 866                curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0
 867                curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0
 868
 869                info.extend([
 870                    "| Previous close price of the instrument:                     | {:<54} |\n".format("{}{}".format(
 871                        "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A",
 872                        "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 873                    )),
 874                    "| Last deal price of the instrument:                          | {:<54} |\n".format("{}{}".format(
 875                        "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A",
 876                        "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 877                    )),
 878                    "| Changes between last deal price and last close              | {:<54} |\n".format(
 879                        "{:.2f}%{}".format(
 880                            iJSON["currentPrice"]["changes"],
 881                            " ({}{:.2f} {})".format(
 882                                "+" if bondChangesDelta > 0 else "",
 883                                bondChangesDelta,
 884                                aciCurrency
 885                            ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format(
 886                                "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "",
 887                                iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"],
 888                                currency
 889                            ),
 890                        )
 891                    ),
 892                    "| Current limit price, min / max:                             | {:<54} |\n".format("{}{} / {}{}{}".format(
 893                        "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A",
 894                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 895                        "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A",
 896                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 897                        " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else ""
 898                    )),
 899                    "| Actual price, sell / buy:                                   | {:<54} |\n".format("{}{} / {}{}{}".format(
 900                        "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A",
 901                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 902                        "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A",
 903                        "%" if iJSON["type"] == "Bonds" else" {}".format(currency),
 904                        " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else ""
 905                    )),
 906                ])
 907
 908            if "lot" in iJSON.keys():
 909                info.append("| Minimum lot to buy:                                         | {:<54} |\n".format(iJSON["lot"]))
 910
 911            if "step" in iJSON.keys() and iJSON["step"] != 0:
 912                info.append("| Minimum price increment (step):                             | {:<54} |\n".format(iJSON["step"]))
 913
 914            # Add bond payment calendar:
 915            if iJSON["type"] == "Bonds":
 916                strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False)   # bond payment calendar
 917                info.extend(["\n", strCalendar])
 918
 919            infoText += "".join(info)
 920
 921            if show:
 922                uLogger.info("{}".format(infoText))
 923
 924            else:
 925                uLogger.debug("{}".format(infoText))
 926
 927            if self.infoFile is not None:
 928                with open(self.infoFile, "w", encoding="UTF-8") as fH:
 929                    fH.write(infoText)
 930
 931                uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile)))
 932
 933        return infoText
 934
 935    def SearchByTicker(self, requestPrice: bool = False, show: bool = False, debug: bool = False) -> dict:
 936        """
 937        Search and return raw broker's information about instrument by its ticker.
 938        `ticker` must be defined! If debug=True then print all debug messages.
 939
 940        :param requestPrice: if `False` then do not request current price of instrument (because this is long operation).
 941        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 942        :param debug: if `True` then print all debug console messages.
 943        :return: JSON formatted data with information about instrument.
 944        """
 945        tickerJSON = {}
 946        if debug:
 947            uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self.ticker))
 948
 949        if not self.ticker:
 950            uLogger.warning("self.ticker variable is not be empty!")
 951
 952        else:
 953            if self.ticker in TKS_TICKERS_OR_FIGI_EXCLUDED:
 954                uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self.ticker))
 955                raise Exception("Instrument not allowed")
 956
 957            if not self.iList:
 958                self.iList = self.Listing()
 959
 960            if self.ticker in self.iList["Shares"].keys():
 961                tickerJSON = self.iList["Shares"][self.ticker]
 962                if debug:
 963                    uLogger.debug("Ticker [{}] found in shares list".format(self.ticker))
 964
 965            elif self.ticker in self.iList["Currencies"].keys():
 966                tickerJSON = self.iList["Currencies"][self.ticker]
 967                if debug:
 968                    uLogger.debug("Ticker [{}] found in currencies list".format(self.ticker))
 969
 970            elif self.ticker in self.iList["Bonds"].keys():
 971                tickerJSON = self.iList["Bonds"][self.ticker]
 972                if debug:
 973                    uLogger.debug("Ticker [{}] found in bonds list".format(self.ticker))
 974
 975            elif self.ticker in self.iList["Etfs"].keys():
 976                tickerJSON = self.iList["Etfs"][self.ticker]
 977                if debug:
 978                    uLogger.debug("Ticker [{}] found in etfs list".format(self.ticker))
 979
 980            elif self.ticker in self.iList["Futures"].keys():
 981                tickerJSON = self.iList["Futures"][self.ticker]
 982                if debug:
 983                    uLogger.debug("Ticker [{}] found in futures list".format(self.ticker))
 984
 985        if tickerJSON:
 986            self.figi = tickerJSON["figi"]
 987
 988            if requestPrice:
 989                tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False)
 990
 991                if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None:
 992                    tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"]
 993
 994                else:
 995                    tickerJSON["currentPrice"]["changes"] = 0
 996
 997            if show:
 998                self.ShowInstrumentInfo(iJSON=tickerJSON, show=True)  # print info as Markdown text
 999
1000        else:
1001            if show:
1002                uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self.ticker))
1003
1004        return tickerJSON
1005
1006    def SearchByFIGI(self, requestPrice: bool = False, show: bool = False, debug: bool = False) -> dict:
1007        """
1008        Search and return raw broker's information about instrument by its FIGI.
1009        `figi` must be defined! If debug=True then print all debug messages.
1010
1011        :param requestPrice: if `False` then do not request current price of instrument (it's long operation).
1012        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
1013        :param debug: if `True` then print all debug console messages.
1014        :return: JSON formatted data with information about instrument.
1015        """
1016        figiJSON = {}
1017        if debug:
1018            uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self.figi))
1019
1020        if not self.figi:
1021            uLogger.warning("self.figi variable is not be empty!")
1022
1023        else:
1024            if self.figi in TKS_TICKERS_OR_FIGI_EXCLUDED:
1025                uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self.figi))
1026                raise Exception("Instrument not allowed")
1027
1028            if not self.iList:
1029                self.iList = self.Listing()
1030
1031            for item in self.iList["Shares"].keys():
1032                if self.figi == self.iList["Shares"][item]["figi"]:
1033                    figiJSON = self.iList["Shares"][item]
1034
1035                    if debug:
1036                        uLogger.debug("FIGI [{}] found in shares list".format(self.figi))
1037
1038                    break
1039
1040            if not figiJSON:
1041                for item in self.iList["Currencies"].keys():
1042                    if self.figi == self.iList["Currencies"][item]["figi"]:
1043                        figiJSON = self.iList["Currencies"][item]
1044
1045                        if debug:
1046                            uLogger.debug("FIGI [{}] found in currencies list".format(self.figi))
1047
1048                        break
1049
1050            if not figiJSON:
1051                for item in self.iList["Bonds"].keys():
1052                    if self.figi == self.iList["Bonds"][item]["figi"]:
1053                        figiJSON = self.iList["Bonds"][item]
1054
1055                        if debug:
1056                            uLogger.debug("FIGI [{}] found in bonds list".format(self.figi))
1057
1058                        break
1059
1060            if not figiJSON:
1061                for item in self.iList["Etfs"].keys():
1062                    if self.figi == self.iList["Etfs"][item]["figi"]:
1063                        figiJSON = self.iList["Etfs"][item]
1064
1065                        if debug:
1066                            uLogger.debug("FIGI [{}] found in etfs list".format(self.figi))
1067
1068                        break
1069
1070            if not figiJSON:
1071                for item in self.iList["Futures"].keys():
1072                    if self.figi == self.iList["Futures"][item]["figi"]:
1073                        figiJSON = self.iList["Futures"][item]
1074
1075                        if debug:
1076                            uLogger.debug("FIGI [{}] found in futures list".format(self.figi))
1077
1078                        break
1079
1080        if figiJSON:
1081            self.figi = figiJSON["figi"]
1082            self.ticker = figiJSON["ticker"]
1083
1084            if requestPrice:
1085                figiJSON["currentPrice"] = self.GetCurrentPrices(show=False)
1086
1087                if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None:
1088                    figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"]
1089
1090                else:
1091                    figiJSON["currentPrice"]["changes"] = 0
1092
1093            if show:
1094                self.ShowInstrumentInfo(iJSON=figiJSON, show=True)  # print info as Markdown text
1095
1096        else:
1097            if show:
1098                uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self.figi))
1099
1100        return figiJSON
1101
1102    def GetCurrentPrices(self, show: bool = True) -> dict:
1103        """
1104        Get and show Depth of Market with current prices of the instrument. If an error occurred then returns an empty record:
1105        `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`.
1106
1107        See also:
1108
1109        :param show: if `True` then print DOM to log and console.
1110        :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`.
1111        """
1112        prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0}
1113
1114        if self.depth < 1:
1115            uLogger.error("Depth of Market (DOM) must be >=1!")
1116            raise Exception("Incorrect value")
1117
1118        if not (self.ticker or self.figi):
1119            uLogger.error("self.ticker or self.figi variables must be defined!")
1120            raise Exception("Ticker or FIGI required")
1121
1122        if self.ticker and not self.figi:
1123            instrumentByTicker = self.SearchByTicker(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1124            self.figi = instrumentByTicker["figi"] if instrumentByTicker else ""
1125
1126        if not self.ticker and self.figi:
1127            instrumentByFigi = self.SearchByFIGI(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1128            self.ticker = instrumentByFigi["ticker"] if instrumentByFigi else ""
1129
1130        if not self.figi:
1131            uLogger.error("FIGI is not defined!")
1132            raise Exception("Ticker or FIGI required")
1133
1134        else:
1135            uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self.ticker, self.figi))
1136
1137            # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1138            priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook"
1139            self.body = str({"figi": self.figi, "depth": self.depth})
1140            pricesResponse = self.SendAPIRequest(priceURL, reqType="POST")
1141
1142            if pricesResponse:
1143                # list of dicts with sellers orders:
1144                prices["buy"] = [{"price": NanoToFloat(item["price"]["units"], item["price"]["nano"]), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]]
1145
1146                # list of dicts with buyers orders:
1147                prices["sell"] = [{"price": NanoToFloat(item["price"]["units"], item["price"]["nano"]), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]]
1148
1149                # max price of instrument at this time:
1150                prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None
1151
1152                # min price of instrument at this time:
1153                prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None
1154
1155                # last price of deal with instrument:
1156                prices["lastPrice"] = NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]) if "lastPrice" in pricesResponse.keys() else 0
1157
1158                # last close price of instrument:
1159                prices["closePrice"] = NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]) if "closePrice" in pricesResponse.keys() else 0
1160
1161            else:
1162                uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi))
1163                uLogger.debug("Server response: {}".format(pricesResponse))
1164
1165            if show:
1166                if prices["buy"] or prices["sell"]:
1167                    info = [
1168                        "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format(
1169                            datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
1170                            self.ticker,
1171                            self.figi,
1172                            self.depth,
1173                        ),
1174                        uLog.sepShort, "\n",
1175                        " Orders of Buyers   | Orders of Sellers\n",
1176                        uLog.sepShort, "\n",
1177                        " Sell prices (vol.) | Buy prices (vol.)\n",
1178                        uLog.sepShort, "\n",
1179                    ]
1180
1181                    if not prices["buy"]:
1182                        info.append("                    | No orders!\n")
1183                        sumBuy = 0
1184
1185                    else:
1186                        sumBuy = sum([x["quantity"] for x in prices["buy"]])
1187                        maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True)
1188                        for item in maxMinSorted:
1189                            info.append("                    | {} ({})\n".format(item["price"], item["quantity"]))
1190
1191                    if not prices["sell"]:
1192                        info.append("No orders!          |\n")
1193                        sumSell = 0
1194
1195                    else:
1196                        sumSell = sum([x["quantity"] for x in prices["sell"]])
1197                        for item in prices["sell"]:
1198                            info.append("{:>19} |\n".format("{} ({})".format(item["price"], item["quantity"])))
1199
1200                    info.extend([
1201                        uLog.sepShort, "\n",
1202                        "{:>19} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)),
1203                        uLog.sepShort, "\n",
1204                    ])
1205
1206                    infoText = "".join(info)
1207
1208                    uLogger.info("Current prices in order book:\n\n{}".format(infoText))
1209
1210                else:
1211                    uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi))
1212
1213        return prices
1214
1215    def ShowInstrumentsInfo(self, show: bool = True) -> str:
1216        """
1217        This method get and show information about all available broker instruments for current user account.
1218        If `instrumentsFile` string is not empty then also save information to this file.
1219
1220        :param show: if `True` then print results to console, if `False` - print only to file.
1221        :return: multi-lines string with all available broker instruments
1222        """
1223        if not self.iList:
1224            self.iList = self.Listing()
1225
1226        info = [
1227            "# All available instruments from Tinkoff Broker server for current user token\n\n",
1228            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1229        ]
1230
1231        # add instruments count by type:
1232        for iType in self.iList.keys():
1233            info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType])))
1234
1235        headerLine = "| Ticker       | Full name                                                 | FIGI         | Cur | Lot     | Step       |\n"
1236        splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n"
1237
1238        # generating info tables with all instruments by type:
1239        for iType in self.iList.keys():
1240            info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine])
1241
1242            for instrument in self.iList[iType].keys():
1243                iName = self.iList[iType][instrument]["name"]  # instrument's name
1244                if len(iName) > 57:
1245                    iName = "{}...".format(iName[:54])  # right trim for a long string
1246
1247                info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format(
1248                    self.iList[iType][instrument]["ticker"],
1249                    iName,
1250                    self.iList[iType][instrument]["figi"],
1251                    self.iList[iType][instrument]["currency"],
1252                    self.iList[iType][instrument]["lot"],
1253                    "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0,
1254                ))
1255
1256        infoText = "".join(info)
1257
1258        if show:
1259            uLogger.info(infoText)
1260
1261        if self.instrumentsFile:
1262            with open(self.instrumentsFile, "w", encoding="UTF-8") as fH:
1263                fH.write(infoText)
1264
1265            uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile)))
1266
1267        return infoText
1268
1269    def SearchInstruments(self, pattern: str, show: bool = True) -> dict:
1270        """
1271        This method search and show information about instruments by part of its ticker, FIGI or name.
1272        If `searchResultsFile` string is not empty then also save information to this file.
1273
1274        :param pattern: string with part of ticker, FIGI or instrument's name.
1275        :param show: if `True` then print results to console, if `False` - return list of result only.
1276        :return: list of dictionaries with all found instruments.
1277        """
1278        if not self.iList:
1279            self.iList = self.Listing()
1280
1281        searchResults = {iType: {} for iType in self.iList}  # same as iList but will contains only filtered instruments
1282        compiledPattern = re.compile(pattern, re.IGNORECASE)
1283
1284        for iType in self.iList:
1285            for instrument in self.iList[iType].values():
1286                searchResult = compiledPattern.search(" ".join(
1287                    [instrument["ticker"], instrument["figi"], instrument["name"]]
1288                ))
1289
1290                if searchResult:
1291                    searchResults[iType][instrument["ticker"]] = instrument
1292
1293        resultsLen = sum([len(searchResults[iType]) for iType in searchResults])
1294        info = [
1295            "# Search results\n\n",
1296            "* **Search pattern:** [{}]\n".format(pattern),
1297            "* **Found instruments:** [{}]\n\n".format(resultsLen),
1298            "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n"
1299        ]
1300        infoShort = info[:]
1301
1302        headerLine = "| Type       | Ticker       | Full name                                                      | FIGI         |\n"
1303        splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n"
1304        skippedLine = "| ...        | ...          | ...                                                            | ...          |\n"
1305
1306        if resultsLen == 0:
1307            info.append("\nNo results\n")
1308            infoShort.append("\nNo results\n")
1309            uLogger.warning("No results. Try changing your search pattern.")
1310
1311        else:
1312            for iType in searchResults:
1313                iTypeValuesCount = len(searchResults[iType].values())
1314                if iTypeValuesCount > 0:
1315                    info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1316                    infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1317
1318                    for instrument in searchResults[iType].values():
1319                        info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format(
1320                            instrument["type"],
1321                            instrument["ticker"],
1322                            "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"],  # right trim for a long string
1323                            instrument["figi"],
1324                        ))
1325
1326                    if iTypeValuesCount <= 5:
1327                        infoShort.extend(info[-iTypeValuesCount:])
1328
1329                    else:
1330                        infoShort.extend(info[-5:])
1331                        infoShort.append(skippedLine)
1332
1333        infoText = "".join(info)
1334        infoTextShort = "".join(infoShort)
1335
1336        if show:
1337            uLogger.info(infoTextShort)
1338            uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`")
1339
1340        if self.searchResultsFile:
1341            with open(self.searchResultsFile, "w", encoding="UTF-8") as fH:
1342                fH.write(infoText)
1343
1344            uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile)))
1345
1346        return searchResults
1347
1348    def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1349        """
1350        Creating list with unique instrument FIGIs from input list of tickers or FIGIs.
1351
1352        :param instruments: list of strings with tickers or FIGIs.
1353        :return: list with unique instrument FIGIs only.
1354        """
1355        requestedInstruments = []
1356        for iName in instruments:
1357            if iName not in self.aliases.keys():
1358                if iName not in requestedInstruments:
1359                    requestedInstruments.append(iName)
1360
1361            else:
1362                if iName not in requestedInstruments:
1363                    if self.aliases[iName] not in requestedInstruments:
1364                        requestedInstruments.append(self.aliases[iName])
1365
1366        uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments))
1367
1368        onlyUniqueFIGIs = []
1369        for iName in requestedInstruments:
1370            if iName in TKS_TICKERS_OR_FIGI_EXCLUDED:
1371                continue
1372
1373            self.ticker = iName
1374            iData = self.SearchByTicker(requestPrice=False)  # trying to find instrument by ticker
1375
1376            if not iData:
1377                self.ticker = ""
1378                self.figi = iName
1379
1380                iData = self.SearchByFIGI(requestPrice=False)  # trying to find instrument by FIGI
1381
1382                if not iData:
1383                    self.figi = ""
1384                    uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName))
1385
1386            if iData and iData["figi"] not in onlyUniqueFIGIs:
1387                onlyUniqueFIGIs.append(iData["figi"])
1388
1389        uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs))
1390
1391        return onlyUniqueFIGIs
1392
1393    def GetListOfPrices(self, instruments: list, show: bool = False) -> list:
1394        """
1395        This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
1396        See limits: https://tinkoff.github.io/investAPI/limits/
1397        If `pricesFile` string is not empty then also save information to this file.
1398
1399        :param instruments: list of strings with tickers or FIGIs.
1400        :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`.
1401        :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1402                 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods.
1403        """
1404        if instruments is None or not instruments:
1405            uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!")
1406            raise Exception("Ticker or FIGI required")
1407
1408        onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments)
1409
1410        uLogger.debug("Requesting current prices from Tinkoff Broker server...")
1411
1412        iList = []  # trying to get info and current prices about all unique instruments:
1413        for self.figi in onlyUniqueFIGIs:
1414            iData = self.SearchByFIGI(requestPrice=True)
1415            iList.append(iData)
1416
1417        self.ShowListOfPrices(iList, show)
1418
1419        return iList
1420
1421    def ShowListOfPrices(self, iList: list, show: bool = True) -> str:
1422        """
1423        Show table contains current prices of given instruments.
1424
1425        :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1426                      One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods.
1427        :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`.
1428        :return: multilines text in Markdown format as a table contains current prices.
1429        """
1430        infoText = ""
1431
1432        if show or self.pricesFile:
1433            info = [
1434                "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1435                "| Ticker       | FIGI         | Type       | Prev. close | Last price  | Chg. %   | Day limits min/max  | Actual sell / buy   | Curr. |\n",
1436                "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n",
1437            ]
1438
1439            for item in iList:
1440                info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format(
1441                    item["ticker"],
1442                    item["figi"],
1443                    item["type"],
1444                    "{:.2f}".format(float(item["currentPrice"]["closePrice"])),
1445                    "{:.2f}".format(float(item["currentPrice"]["lastPrice"])),
1446                    "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])),
1447                    "{} / {}".format(
1448                        item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A",
1449                        item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A",
1450                    ),
1451                    "{} / {}".format(
1452                        item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A",
1453                        item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A",
1454                    ),
1455                    item["currency"],
1456                ))
1457
1458            infoText = "".join(info)
1459
1460            if show:
1461                uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText))
1462
1463            if self.pricesFile:
1464                with open(self.pricesFile, "w", encoding="UTF-8") as fH:
1465                    fH.write(infoText)
1466
1467                uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile)))
1468
1469        return infoText
1470
1471    def RequestTradingStatus(self) -> dict:
1472        """
1473        Requesting trading status for the instrument defined by `figi` variable.
1474        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus
1475        Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
1476
1477        :return: dictionary with trading status attributes. Response example:
1478                 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING",
1479                  "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}`
1480        """
1481        if self.figi is None or not self.figi:
1482            uLogger.error("Variable `figi` must be defined for using this method!")
1483            raise Exception("FIGI required")
1484
1485        uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self.figi))
1486
1487        self.body = str({"figi": self.figi, "instrumentId": self.figi})
1488        tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus"
1489        tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST")
1490
1491        uLogger.debug("Records about current trading status successfully received")
1492
1493        return tradingStatus
1494
1495    def RequestPortfolio(self) -> dict:
1496        """
1497        Requesting actual user's portfolio for current `accountId`.
1498        REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
1499        Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
1500
1501        :return: dictionary with user's portfolio.
1502        """
1503        if self.accountId is None or not self.accountId:
1504            uLogger.error("Variable `accountId` must be defined for using this method!")
1505            raise Exception("Account ID required")
1506
1507        uLogger.debug("Requesting current actual user's portfolio. Wait, please...")
1508
1509        self.body = str({"accountId": self.accountId})
1510        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio"
1511        rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST")
1512
1513        uLogger.debug("Records about user's portfolio successfully received")
1514
1515        return rawPortfolio
1516
1517    def RequestPositions(self) -> dict:
1518        """
1519        Requesting open positions by currencies and instruments for current `accountId`.
1520        REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
1521        Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
1522
1523        :return: dictionary with open positions by instruments.
1524        """
1525        if self.accountId is None or not self.accountId:
1526            uLogger.error("Variable `accountId` must be defined for using this method!")
1527            raise Exception("Account ID required")
1528
1529        uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...")
1530
1531        self.body = str({"accountId": self.accountId})
1532        positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions"
1533        rawPositions = self.SendAPIRequest(positionsURL, reqType="POST")
1534
1535        uLogger.debug("Records about current open positions successfully received")
1536
1537        return rawPositions
1538
1539    def RequestPendingOrders(self) -> list:
1540        """
1541        Requesting current actual pending orders for current `accountId`.
1542        REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
1543        Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
1544
1545        :return: list of dictionaries with pending orders.
1546        """
1547        if self.accountId is None or not self.accountId:
1548            uLogger.error("Variable `accountId` must be defined for using this method!")
1549            raise Exception("Account ID required")
1550
1551        uLogger.debug("Requesting current actual pending orders. Wait, please...")
1552
1553        self.body = str({"accountId": self.accountId})
1554        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders"
1555        rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"]
1556
1557        uLogger.debug("[{}] records about pending orders received".format(len(rawOrders)))
1558
1559        return rawOrders
1560
1561    def RequestStopOrders(self) -> list:
1562        """
1563        Requesting current actual stop orders for current `accountId`.
1564        REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
1565        Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
1566
1567        :return: list of dictionaries with stop orders.
1568        """
1569        if self.accountId is None or not self.accountId:
1570            uLogger.error("Variable `accountId` must be defined for using this method!")
1571            raise Exception("Account ID required")
1572
1573        uLogger.debug("Requesting current actual stop orders. Wait, please...")
1574
1575        self.body = str({"accountId": self.accountId})
1576        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders"
1577        rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"]
1578
1579        uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders)))
1580
1581        return rawStopOrders
1582
1583    def Overview(self, show: bool = False, details: str = "full") -> dict:
1584        """
1585        Get portfolio: all open positions, orders and some statistics for current `accountId`.
1586        If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile`
1587        are defined then also save information to file.
1588
1589        WARNING! It is not recommended to run this method too many times in a loop! The server receives
1590        many requests about the state of the portfolio, and then, based on the received data, a large number
1591        of calculation and statistics are collected.
1592
1593        :param show: if `False` then only dictionary returns, if `True` then show more debug information.
1594        :param details: how detailed should the information be? You should specify one of strings:
1595                        `full` - shows full available information about portfolio status (by default),
1596                        `positions` - shows only open positions,
1597                        `digest` - show a short digest of the portfolio status,
1598                        `analytics` - shows only the analytics section and the distribution of the portfolio by various categories,
1599                        `orders` - shows only sections of open limits and stop orders.
1600        :return: dictionary with client's raw portfolio and some statistics.
1601        """
1602        if self.accountId is None or not self.accountId:
1603            uLogger.error("Variable `accountId` must be defined for using this method!")
1604            raise Exception("Account ID required")
1605
1606        view = {
1607            "raw": {  # --- raw portfolio responses from broker with user portfolio data:
1608                "headers": {},  # list of dictionaries, response headers without "positions" section
1609                "Currencies": [],  # list of dictionaries, open trades with currencies from "positions" section
1610                "Shares": [],  # list of dictionaries, open trades with shares from "positions" section
1611                "Bonds": [],  # list of dictionaries, open trades with bonds from "positions" section
1612                "Etfs": [],  # list of dictionaries, open trades with etfs from "positions" section
1613                "Futures": [],  # list of dictionaries, open trades with futures from "positions" section
1614                "positions": {},  # raw response from broker: dictionary with current available or blocked currencies and instruments for client
1615                "orders": [],  # raw response from broker: list of dictionaries with all pending (market) orders
1616                "stopOrders": [],  # raw response from broker: list of dictionaries with all stop orders
1617                "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}},  # dict with prices of all currencies in RUB
1618            },
1619            "stat": {  # --- some statistics calculated using "raw" sections:
1620                "portfolioCostRUB": 0.,  # portfolio cost in RUB (Russian Rouble)
1621                "availableRUB": 0.,  # available rubles (without other currencies)
1622                "blockedRUB": 0.,  # blocked sum in Russian Rouble
1623                "totalChangesRUB": 0.,  # changes for all open trades in RUB
1624                "totalChangesPercentRUB": 0.,  # changes for all open trades in percents
1625                "allCurrenciesCostRUB": 0.,  # costs of all currencies (include rubles) in RUB
1626                "sharesCostRUB": 0.,  # costs of all shares in RUB
1627                "bondsCostRUB": 0.,  # costs of all bonds in RUB
1628                "etfsCostRUB": 0.,  # costs of all etfs in RUB
1629                "futuresCostRUB": 0.,  # costs of all futures in RUB
1630                "Currencies": [],  # list of dictionaries of all currencies statistics
1631                "Shares": [],  # list of dictionaries of all shares statistics
1632                "Bonds": [],  # list of dictionaries of all bonds statistics
1633                "Etfs": [],  # list of dictionaries of all etfs statistics
1634                "Futures": [],  # list of dictionaries of all futures statistics
1635                "orders": [],  # list of dictionaries of all pending (market) orders and it's parameters
1636                "stopOrders": [],  # list of dictionaries of all stop orders and it's parameters
1637                "blockedCurrencies": {},  # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21}
1638                "blockedInstruments": {},  # dict with blocked  by FIGI, e.g. {}
1639                "funds": {},  # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1640            },
1641            "analytics": {  # --- some analytics of portfolio:
1642                "distrByAssets": {},  # portfolio distribution by assets
1643                "distrByCompanies": {},  # portfolio distribution by companies
1644                "distrBySectors": {},  # portfolio distribution by sectors
1645                "distrByCurrencies": {},  # portfolio distribution by currencies
1646                "distrByCountries": {},  # portfolio distribution by countries
1647            }
1648        }
1649
1650        details = details.lower()
1651        availableDetails = ["full", "positions", "digest", "analytics", "orders"]
1652        if details not in availableDetails:
1653            details = "full"
1654            uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails))
1655
1656        uLogger.debug("Requesting portfolio of a client. Wait, please...")
1657
1658        portfolioResponse = self.RequestPortfolio()  # current user's portfolio (dict)
1659        view["raw"]["positions"] = self.RequestPositions()  # current open positions by instruments (dict)
1660        view["raw"]["orders"] = self.RequestPendingOrders()  # current actual pending orders (list)
1661        view["raw"]["stopOrders"] = self.RequestStopOrders()  # current actual stop orders (list)
1662
1663        # save response headers without "positions" section:
1664        for key in portfolioResponse.keys():
1665            if key != "positions":
1666                view["raw"]["headers"][key] = portfolioResponse[key]
1667
1668            else:
1669                continue
1670
1671        # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation
1672        # Type of instrument must be only one of supported types in TKS_INSTRUMENTS
1673        for item in portfolioResponse["positions"]:
1674            if item["instrumentType"] == "currency":
1675                self.figi = item["figi"]
1676                curr = self.SearchByFIGI(requestPrice=False)
1677
1678                # current price of currency in RUB:
1679                view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = {
1680                    "name": curr["name"],
1681                    "currentPrice": NanoToFloat(
1682                        item["currentPrice"]["units"],
1683                        item["currentPrice"]["nano"]
1684                    ),
1685                }
1686
1687                view["raw"]["Currencies"].append(item)
1688
1689            elif item["instrumentType"] == "share":
1690                view["raw"]["Shares"].append(item)
1691
1692            elif item["instrumentType"] == "bond":
1693                view["raw"]["Bonds"].append(item)
1694
1695            elif item["instrumentType"] == "etf":
1696                view["raw"]["Etfs"].append(item)
1697
1698            elif item["instrumentType"] == "futures":
1699                view["raw"]["Futures"].append(item)
1700
1701            else:
1702                continue
1703
1704        # how many volume of currencies (by ISO currency name) are blocked:
1705        for item in view["raw"]["positions"]["blocked"]:
1706            blocked = NanoToFloat(item["units"], item["nano"])
1707            if blocked > 0:
1708                view["stat"]["blockedCurrencies"][item["currency"]] = blocked
1709
1710        # how many volume of instruments (by FIGI) are blocked:
1711        for item in view["raw"]["positions"]["securities"]:
1712            blocked = int(item["blocked"])
1713            if blocked > 0:
1714                view["stat"]["blockedInstruments"][item["figi"]] = blocked
1715
1716        allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]}
1717
1718        if "rub" in allBlocked.keys():
1719            view["stat"]["blockedRUB"] = allBlocked["rub"]  # blocked rubles
1720
1721        # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies:
1722        view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"])
1723        view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"])
1724        view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"])
1725        view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"])
1726        view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"])
1727        view["stat"]["portfolioCostRUB"] = sum([
1728            view["stat"]["allCurrenciesCostRUB"],
1729            view["stat"]["sharesCostRUB"],
1730            view["stat"]["bondsCostRUB"],
1731            view["stat"]["etfsCostRUB"],
1732            view["stat"]["futuresCostRUB"],
1733        ])
1734
1735        # --- calculating some portfolio statistics:
1736        byComp = {}  # distribution by companies
1737        bySect = {}  # distribution by sectors
1738        byCurr = {}  # distribution by currencies (include RUB)
1739        unknownCountryName = "All other countries"  # default name for instruments without "countryOfRisk" and "countryOfRiskName"
1740        byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}}  # distribution by countries (currencies are included in their countries)
1741
1742        for item in portfolioResponse["positions"]:
1743            self.figi = item["figi"]
1744            instrument = self.SearchByFIGI(requestPrice=False)  # full raw info about instrument by FIGI
1745
1746            if instrument:
1747                if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys():
1748                    blocked = allBlocked[instrument["nominal"]["currency"]]  # blocked volume of currency
1749
1750                elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys():
1751                    blocked = allBlocked[item["figi"]]  # blocked volume of other instruments
1752
1753                else:
1754                    blocked = 0
1755
1756                volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"])  # available volume of instrument
1757                lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"])  # available volume in lots of instrument
1758                direction = "Long" if lots >= 0 else "Short"  # direction of an instrument's position: short or long
1759                curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"])  # current instrument's price
1760                average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"])  # current average position price
1761                profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"])  # expected profit at current moment
1762                currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"]  # currency name rub, usd, eur etc.
1763                cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume  # current cost of all volume of instrument in basic asset
1764                baseCurrencyName = item["currentPrice"]["currency"]  # name of base currency (rub)
1765                countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName
1766                costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"]  # cost in rubles
1767                percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.  # instrument's part in percent of full portfolio cost
1768
1769                statData = {
1770                    "figi": item["figi"],  # FIGI from REST API "GetPortfolio" method
1771                    "ticker": instrument["ticker"],  # ticker by FIGI
1772                    "currency": currency,  # currency name rub, usd, eur etc. for instrument price
1773                    "volume": volume,  # available volume of instrument
1774                    "lots": lots,  # volume in lots of instrument
1775                    "direction": direction,  # direction of an instrument's position: short or long
1776                    "blocked": blocked,  # blocked volume of currency or instrument
1777                    "currentPrice": curPrice,  # current instrument's price in basic asset
1778                    "average": average,  # current average position price
1779                    "cost": cost,  # current cost of all volume of instrument in basic asset
1780                    "baseCurrencyName": baseCurrencyName,  # name of base currency (rub)
1781                    "costRUB": costRUB,  # cost of instrument in ruble
1782                    "percentCostRUB": percentCostRUB,  # instrument's part in percent of full portfolio cost in RUB
1783                    "profit": profit,  # expected profit at current moment
1784                    "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0,  # expected percents of profit at current moment for this instrument
1785                    "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other",
1786                    "name": instrument["name"] if "name" in instrument.keys() else "",  # human-readable names of instruments
1787                    "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "",  # ISO name for currencies only
1788                    "country": countryName,  # e.g. "[RU] Российская Федерация" or unknownCountryName
1789                    "step": instrument["step"],  # minimum price increment
1790                }
1791
1792                # adding distribution by unique countries:
1793                if statData["country"] not in byCountry.keys():
1794                    byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB}
1795
1796                else:
1797                    byCountry[statData["country"]]["cost"] += costRUB
1798                    byCountry[statData["country"]]["percent"] += percentCostRUB
1799
1800                if item["instrumentType"] != "currency":
1801                    # adding distribution by unique companies:
1802                    if statData["name"]:
1803                        if statData["name"] not in byComp.keys():
1804                            byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB}
1805
1806                        else:
1807                            byComp[statData["name"]]["cost"] += costRUB
1808                            byComp[statData["name"]]["percent"] += percentCostRUB
1809
1810                    # adding distribution by unique sectors:
1811                    if statData["sector"] not in bySect.keys():
1812                        bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB}
1813
1814                    else:
1815                        bySect[statData["sector"]]["cost"] += costRUB
1816                        bySect[statData["sector"]]["percent"] += percentCostRUB
1817
1818                # adding distribution by unique currencies:
1819                if currency not in byCurr.keys():
1820                    byCurr[currency] = {
1821                        "name": view["raw"]["currenciesCurrentPrices"][currency]["name"],
1822                        "cost": costRUB,
1823                        "percent": percentCostRUB
1824                    }
1825
1826                else:
1827                    byCurr[currency]["cost"] += costRUB
1828                    byCurr[currency]["percent"] += percentCostRUB
1829
1830                # saving statistics for every instrument:
1831                if item["instrumentType"] == "currency":
1832                    view["stat"]["Currencies"].append(statData)
1833
1834                    # update dict with free funds for trading (total - blocked) by currencies
1835                    # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1836                    view["stat"]["funds"][currency] = {
1837                        "total": volume,
1838                        "totalCostRUB": costRUB,  # total volume cost in rubles
1839                        "free": volume - blocked,
1840                        "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0,  # free volume cost in rubles
1841                    }
1842
1843                elif item["instrumentType"] == "share":
1844                    view["stat"]["Shares"].append(statData)
1845
1846                elif item["instrumentType"] == "bond":
1847                    view["stat"]["Bonds"].append(statData)
1848
1849                elif item["instrumentType"] == "etf":
1850                    view["stat"]["Etfs"].append(statData)
1851
1852                elif item["instrumentType"] == "Futures":
1853                    view["stat"]["Futures"].append(statData)
1854
1855                else:
1856                    continue
1857
1858        # total changes in Russian Ruble:
1859        view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]])  # available RUB without other currencies
1860        view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0.
1861        startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100)
1862        view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost
1863        view["stat"]["funds"]["rub"] = {
1864            "total": view["stat"]["availableRUB"],
1865            "totalCostRUB": view["stat"]["availableRUB"],
1866            "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1867            "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1868        }
1869
1870        # --- pending orders sector data:
1871        uniquePendingOrders = []
1872        uniquePendingOrdersFIGIs = []
1873        for item in view["raw"]["orders"]:
1874            if item["figi"] not in uniquePendingOrdersFIGIs:
1875                uniquePendingOrdersFIGIs.append(item["figi"])
1876                uniquePendingOrders.append(item)
1877
1878        for item in uniquePendingOrders:
1879            self.figi = item["figi"]
1880            instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI
1881
1882            if instrument:
1883                action = TKS_ORDER_DIRECTIONS[item["direction"]]
1884                orderType = TKS_ORDER_TYPES[item["orderType"]]
1885                orderState = TKS_ORDER_STATES[item["executionReportStatus"]]
1886                orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1887
1888                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1889                if item["direction"] == "ORDER_DIRECTION_BUY":
1890                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1891
1892                else:
1893                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1894
1895                # requested price for order execution:
1896                target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"])
1897
1898                # necessary changes in percent to reach target from current price:
1899                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1900
1901                view["stat"]["orders"].append({
1902                    "orderID": item["orderId"],  # orderId number parameter of current order
1903                    "figi": item["figi"],  # FIGI identification
1904                    "ticker": instrument["ticker"],  # ticker name by FIGI
1905                    "lotsRequested": item["lotsRequested"],  # requested lots value
1906                    "lotsExecuted": item["lotsExecuted"],  # how many lots are executed
1907                    "currentPrice": lastPrice,  # current instrument's price for defined action
1908                    "targetPrice": target,  # requested price for order execution in base currency
1909                    "baseCurrencyName": item["initialSecurityPrice"]["currency"],  # name of base currency
1910                    "percentChanges": changes,  # changes in percent to target from current price
1911                    "currency": item["currency"],  # instrument's currency name
1912                    "action": action,  # sell / buy / Unknown from TKS_ORDER_DIRECTIONS
1913                    "type": orderType,  # type of order from TKS_ORDER_TYPES
1914                    "status": orderState,  # order status from TKS_ORDER_STATES
1915                    "date": orderDate,  # string with order date and time from UTC format (without nano seconds part)
1916                })
1917
1918        # --- stop orders sector data:
1919        uniqueStopOrders = []
1920        uniqueStopOrdersFIGIs = []
1921        for item in view["raw"]["stopOrders"]:
1922            if item["figi"] not in uniqueStopOrdersFIGIs:
1923                uniqueStopOrdersFIGIs.append(item["figi"])
1924                uniqueStopOrders.append(item)
1925
1926        for item in uniqueStopOrders:
1927            self.figi = item["figi"]
1928            instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI
1929
1930            if instrument:
1931                action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]]
1932                orderType = TKS_STOP_ORDER_TYPES[item["orderType"]]
1933                createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1934
1935                # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order
1936                if "expirationTime" in item.keys():
1937                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"]
1938                    expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0]
1939
1940                else:
1941                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"]
1942                    expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"]
1943
1944                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1945                if item["direction"] == "STOP_ORDER_DIRECTION_BUY":
1946                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1947
1948                else:
1949                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1950
1951                # requested price when stop-order executed:
1952                target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"])
1953
1954                # price for limit-order, set up when stop-order executed:
1955                limit = NanoToFloat(item["price"]["units"], item["price"]["nano"])
1956
1957                # necessary changes in percent to reach target from current price:
1958                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1959
1960                view["stat"]["stopOrders"].append({
1961                    "orderID": item["stopOrderId"],  # stopOrderId number parameter of current stop-order
1962                    "figi": item["figi"],  # FIGI identification
1963                    "ticker": instrument["ticker"],  # ticker name by FIGI
1964                    "lotsRequested": item["lotsRequested"],  # requested lots value
1965                    "currentPrice": lastPrice,  # current instrument's price for defined action
1966                    "targetPrice": target,  # requested price for stop-order execution in base currency
1967                    "limitPrice": limit,  # price for limit-order, set up when stop-order executed, 0 if market order
1968                    "baseCurrencyName": item["stopPrice"]["currency"],  # name of base currency
1969                    "percentChanges": changes,  # changes in percent to target from current price
1970                    "currency": item["currency"],  # instrument's currency name
1971                    "action": action,  # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS
1972                    "type": orderType,  # type of order from TKS_STOP_ORDER_TYPES
1973                    "expType": expType,  # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES
1974                    "createDate": createDate,  # string with created order date and time from UTC format (without nano seconds part)
1975                    "expDate": expDate,  # string with expiration order date and time from UTC format (without nano seconds part)
1976                })
1977
1978        # --- calculating data for analytics section:
1979        # portfolio distribution by assets:
1980        view["analytics"]["distrByAssets"] = {
1981            "Ruble": {
1982                "uniques": 1,
1983                "cost": view["stat"]["availableRUB"],
1984                "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1985            },
1986            "Currencies": {
1987                "uniques": len(view["stat"]["Currencies"]),  # all foreign currencies without RUB
1988                "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"],
1989                "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1990            },
1991            "Shares": {
1992                "uniques": len(view["stat"]["Shares"]),
1993                "cost": view["stat"]["sharesCostRUB"],
1994                "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1995            },
1996            "Bonds": {
1997                "uniques": len(view["stat"]["Bonds"]),
1998                "cost": view["stat"]["bondsCostRUB"],
1999                "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2000            },
2001            "Etfs": {
2002                "uniques": len(view["stat"]["Etfs"]),
2003                "cost": view["stat"]["etfsCostRUB"],
2004                "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2005            },
2006            "Futures": {
2007                "uniques": len(view["stat"]["Futures"]),
2008                "cost": view["stat"]["futuresCostRUB"],
2009                "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2010            },
2011        }
2012
2013        # portfolio distribution by companies:
2014        view["analytics"]["distrByCompanies"]["All money cash"] = {
2015            "ticker": "",
2016            "cost": view["stat"]["allCurrenciesCostRUB"],
2017            "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2018        }
2019        view["analytics"]["distrByCompanies"].update(byComp)
2020
2021        # portfolio distribution by sectors:
2022        view["analytics"]["distrBySectors"]["All money cash"] = {
2023            "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"],
2024            "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"],
2025        }
2026        view["analytics"]["distrBySectors"].update(bySect)
2027
2028        # portfolio distribution by currencies:
2029        if "rub" not in view["analytics"]["distrByCurrencies"].keys():
2030            uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!")
2031            view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0}
2032
2033        view["analytics"]["distrByCurrencies"].update(byCurr)
2034        view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2035        view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2036
2037        # portfolio distribution by countries:
2038        if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys():
2039            uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!")
2040            view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0}
2041
2042        view["analytics"]["distrByCountries"].update(byCountry)
2043        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2044        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2045
2046        # --- Prepare text statistics overview in human-readable:
2047        if show:
2048            # Whatever the value `details`, header not changes:
2049            info = [
2050                "# Client's portfolio\n\n",
2051                "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
2052                "* **Account ID:** [{}]\n".format(self.accountId),
2053            ]
2054
2055            if details in ["full", "positions", "digest"]:
2056                info.extend([
2057                    "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2058                    "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format(
2059                        "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2060                        view["stat"]["totalChangesRUB"],
2061                        "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2062                        view["stat"]["totalChangesPercentRUB"],
2063                    ),
2064                ])
2065
2066            if details in ["full", "positions"]:
2067                info.extend([
2068                    "## Open positions\n\n",
2069                    "| Ticker [FIGI]               | Volume (blocked)                | Lots     | Curr. price  | Avg. price   | Current volume cost | Profit (%)                   |\n",
2070                    "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n",
2071                    "| Ruble                       | {:>31} |          |              |              |                     |                              |\n".format(
2072                        "{:.2f} ({:.2f}) rub".format(
2073                            view["stat"]["availableRUB"],
2074                            view["stat"]["blockedRUB"],
2075                        )
2076                    )
2077                ])
2078
2079                def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list:
2080                    return [
2081                        "|                             |                                 |          |              |              |                     |                              |\n",
2082                        "| {:<27} |                                 |          |              |              | {:>19} |                              |\n".format(
2083                            noTradeStr if noTradeStr else typeStr,
2084                            "" if noTradeStr else "{:.2f} RUB".format(CostRUB),
2085                        ),
2086                    ]
2087
2088                def _InfoStr(data: dict, showCurrencyName: bool = False) -> str:
2089                    return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format(
2090                        "{} [{}]".format(data["ticker"], data["figi"]),
2091                        "{:.2f} ({:.2f}) {}".format(
2092                            data["volume"],
2093                            data["blocked"],
2094                            data["currency"],
2095                        ) if showCurrencyName else "{:.0f} ({:.0f})".format(
2096                            data["volume"],
2097                            data["blocked"],
2098                        ),
2099                        "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]),
2100                        "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a",
2101                        "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a",
2102                        "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]),
2103                        "{}{:.2f} {} ({}{:.2f}%)".format(
2104                            "+" if data["profit"] > 0 else "",
2105                            data["profit"], data["baseCurrencyName"],
2106                            "+" if data["percentProfit"] > 0 else "",
2107                            data["percentProfit"],
2108                        ),
2109                    )
2110
2111                # --- Show currencies section:
2112                if view["stat"]["Currencies"]:
2113                    info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**"))
2114                    for item in view["stat"]["Currencies"]:
2115                        info.append(_InfoStr(item, showCurrencyName=True))
2116
2117                else:
2118                    info.extend(_SplitStr(noTradeStr="**Currencies:** no trades"))
2119
2120                # --- Show shares section:
2121                if view["stat"]["Shares"]:
2122                    info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**"))
2123
2124                    for item in view["stat"]["Shares"]:
2125                        info.append(_InfoStr(item))
2126
2127                else:
2128                    info.extend(_SplitStr(noTradeStr="**Shares:** no trades"))
2129
2130                # --- Show bonds section:
2131                if view["stat"]["Bonds"]:
2132                    info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**"))
2133
2134                    for item in view["stat"]["Bonds"]:
2135                        info.append(_InfoStr(item))
2136
2137                else:
2138                    info.extend(_SplitStr(noTradeStr="**Bonds:** no trades"))
2139
2140                # --- Show etfs section:
2141                if view["stat"]["Etfs"]:
2142                    info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**"))
2143
2144                    for item in view["stat"]["Etfs"]:
2145                        info.append(_InfoStr(item))
2146
2147                else:
2148                    info.extend(_SplitStr(noTradeStr="**Etfs:** no trades"))
2149
2150                # --- Show futures section:
2151                if view["stat"]["Futures"]:
2152                    info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**"))
2153
2154                    for item in view["stat"]["Futures"]:
2155                        info.append(_InfoStr(item))
2156
2157                else:
2158                    info.extend(_SplitStr(noTradeStr="**Futures:** no trades"))
2159
2160            if details in ["full", "orders"]:
2161                # --- Show pending orders section:
2162                if view["stat"]["orders"]:
2163                    info.extend([
2164                        "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])),
2165                        "\n| Ticker [FIGI]               | Order ID       | Lots (exec.) | Current price (% delta) | Target price  | Action    | Type      | Create date (UTC)       |\n",
2166                        "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n",
2167                    ])
2168
2169                    for item in view["stat"]["orders"]:
2170                        info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format(
2171                            "{} [{}]".format(item["ticker"], item["figi"]),
2172                            item["orderID"],
2173                            "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]),
2174                            "{} {} ({}{:.2f}%)".format(
2175                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2176                                item["baseCurrencyName"],
2177                                "+" if item["percentChanges"] > 0 else "",
2178                                float(item["percentChanges"]),
2179                            ),
2180                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2181                            item["action"],
2182                            item["type"],
2183                            item["date"],
2184                        ))
2185
2186                else:
2187                    info.append("\n## Total pending limit-orders: 0\n")
2188
2189                # --- Show stop orders section:
2190                if view["stat"]["stopOrders"]:
2191                    info.extend([
2192                        "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])),
2193                        "\n| Ticker [FIGI]               | Stop order ID                        | Lots   | Current price (% delta) | Target price  | Limit price   | Action    | Type        | Expire type  | Create date (UTC)   | Expiration (UTC)    |\n",
2194                        "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n",
2195                    ])
2196
2197                    for item in view["stat"]["stopOrders"]:
2198                        info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format(
2199                            "{} [{}]".format(item["ticker"], item["figi"]),
2200                            item["orderID"],
2201                            item["lotsRequested"],
2202                            "{} {} ({}{:.2f}%)".format(
2203                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2204                                item["baseCurrencyName"],
2205                                "+" if item["percentChanges"] > 0 else "",
2206                                float(item["percentChanges"]),
2207                            ),
2208                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2209                            "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"],
2210                            item["action"],
2211                            item["type"],
2212                            item["expType"],
2213                            item["createDate"],
2214                            item["expDate"],
2215                        ))
2216
2217                else:
2218                    info.append("\n## Total stop-orders: 0\n")
2219
2220            if details in ["full", "analytics"]:
2221                # -- Show analytics section:
2222                if view["stat"]["portfolioCostRUB"] > 0:
2223                    info.extend([
2224                        "\n# Analytics\n"
2225                        "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2226                        "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format(
2227                            "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2228                            view["stat"]["totalChangesRUB"],
2229                            "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2230                            view["stat"]["totalChangesPercentRUB"],
2231                        ),
2232                        "\n## Portfolio distribution by assets\n"
2233                        "\n| Type       | Uniques | Percent | Current cost       |\n",
2234                        "|------------|---------|---------|--------------------|\n",
2235                    ])
2236
2237                    for key in view["analytics"]["distrByAssets"].keys():
2238                        if view["analytics"]["distrByAssets"][key]["cost"] > 0:
2239                            info.append("| {:<10} | {:<7} | {:<7} | {:<18} |\n".format(
2240                                key,
2241                                view["analytics"]["distrByAssets"][key]["uniques"],
2242                                "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]),
2243                                "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]),
2244                            ))
2245
2246                    maxLenNames = 3 + max([len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"]) for company in view["analytics"]["distrByCompanies"].keys()])
2247                    info.extend([
2248                        "\n## Portfolio distribution by companies\n"
2249                        "\n| Company{} | Percent | Current cost       |\n".format(" " * (maxLenNames - 7)),
2250                        "|--------{}-|---------|--------------------|\n".format("-" * (maxLenNames - 7)),
2251                    ])
2252
2253                    for company in view["analytics"]["distrByCompanies"].keys():
2254                        if view["analytics"]["distrByCompanies"][company]["cost"] > 0:
2255                            nameLen = len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"])
2256                            info.append("| {} | {:<7} | {:<18} |\n".format(
2257                                "{}{}{}".format(
2258                                    "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "",
2259                                    company,
2260                                    "" if nameLen == maxLenNames else "{}".format(" " * (maxLenNames - nameLen - 3) if view["analytics"]["distrByCompanies"][company]["ticker"] else " " * (maxLenNames - nameLen)),
2261                                ),
2262                                "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]),
2263                                "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]),
2264                            ))
2265
2266                    maxLenSectors = max([len(sector) for sector in view["analytics"]["distrBySectors"].keys()])
2267                    info.extend([
2268                        "\n## Portfolio distribution by sectors\n"
2269                        "\n| Sector{} | Percent | Current cost       |\n".format(" " * (maxLenSectors - 6)),
2270                        "|-------{}-|---------|--------------------|\n".format("-" * (maxLenSectors - 6)),
2271                    ])
2272
2273                    for sector in view["analytics"]["distrBySectors"].keys():
2274                        if view["analytics"]["distrBySectors"][sector]["cost"] > 0:
2275                            info.append("| {}{} | {:<7} | {:<18} |\n".format(
2276                                sector,
2277                                "" if len(sector) == maxLenSectors else " " * (maxLenSectors - len(sector)),
2278                                "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]),
2279                                "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]),
2280                            ))
2281
2282                    maxLenMoney = 3 + max([len(currency) + len(view["analytics"]["distrByCurrencies"][currency]["name"]) for currency in view["analytics"]["distrByCurrencies"].keys()])
2283                    info.extend([
2284                        "\n## Portfolio distribution by currencies\n"
2285                        "\n| Instruments currencies{} | Percent | Current cost       |\n".format(" " * (maxLenMoney - 22)),
2286                        "|-----------------------{}-|---------|--------------------|\n".format("-" * (maxLenMoney - 22)),
2287                    ])
2288
2289                    for curr in view["analytics"]["distrByCurrencies"].keys():
2290                        if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0:
2291                            nameLen = 3 + len(curr) + len(view["analytics"]["distrByCurrencies"][curr]["name"])
2292                            info.append("| {} | {:<7} | {:<18} |\n".format(
2293                                "[{}] {}{}".format(
2294                                    curr,
2295                                    view["analytics"]["distrByCurrencies"][curr]["name"],
2296                                    "" if nameLen == maxLenMoney else " " * (maxLenMoney - nameLen),
2297                                ),
2298                                "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]),
2299                                "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]),
2300                            ))
2301
2302                    maxLenCountry = max(17, max([len(country) for country in view["analytics"]["distrByCountries"].keys()]))
2303                    info.extend([
2304                        "\n## Portfolio distribution by countries\n"
2305                        "\n| Assets by country{} | Percent | Current cost       |\n".format(" " * (maxLenCountry - 17)),
2306                        "|------------------{}-|---------|--------------------|\n".format("-" * (maxLenCountry - 17)),
2307                    ])
2308
2309                    for country in view["analytics"]["distrByCountries"].keys():
2310                        if view["analytics"]["distrByCountries"][country]["cost"] > 0:
2311                            nameLen = len(country)
2312                            info.append("| {} | {:<7} | {:<18} |\n".format(
2313                                "{}{}".format(
2314                                    country,
2315                                    "" if nameLen == maxLenCountry else " " * (maxLenCountry - nameLen),
2316                                ),
2317                                "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]),
2318                                "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]),
2319                            ))
2320
2321            infoText = "".join(info)
2322
2323            uLogger.info(infoText)
2324
2325            if details == "full" and self.overviewFile:
2326                filename = self.overviewFile
2327
2328            elif details == "digest" and self.overviewDigestFile:
2329                filename = self.overviewDigestFile
2330
2331            elif details == "positions" and self.overviewPositionsFile:
2332                filename = self.overviewPositionsFile
2333
2334            elif details == "orders" and self.overviewOrdersFile:
2335                filename = self.overviewOrdersFile
2336
2337            elif details == "analytics" and self.overviewAnalyticsFile:
2338                filename = self.overviewAnalyticsFile
2339
2340            else:
2341                filename = ""
2342
2343            if filename:
2344                with open(filename, "w", encoding="UTF-8") as fH:
2345                    fH.write(infoText)
2346
2347                uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename)))
2348
2349        return view
2350
2351    def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple:
2352        """
2353        Returns history operations between two given dates for current `accountId`.
2354        If `reportFile` string is not empty then also save human-readable report.
2355        Shows some statistical data of closed positions.
2356
2357        :param start: see docstring in `GetDatesAsString()` method
2358        :param end: see docstring in `GetDatesAsString()` method
2359        :param show: if `True` then also prints all records to the console.
2360        :param showCancelled: if `False` then remove information about cancelled operations from the deals report.
2361        :return: original list of dictionaries with history of deals records from API ("operations" key):
2362                 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2363                 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2364        """
2365        if self.accountId is None or not self.accountId:
2366            uLogger.error("Variable `accountId` must be defined for using this method!")
2367            raise Exception("Account ID required")
2368
2369        startDate, endDate = GetDatesAsString(start, end)  # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2370
2371        uLogger.debug("Requesting history of a client's operations. Wait, please...")
2372
2373        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2374        dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations"
2375        self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate})
2376        ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"]  # list of dict: operations returns by broker
2377        customStat = {}  # custom statistics in additional to responseJSON
2378
2379        # --- output report in human-readable format:
2380        if show or self.reportFile:
2381            splitLine1 = "|                            |                               |                              |                      |                        |\n"  # Summary section
2382            splitLine2 = "|                     |              |              |            |           |                 |            |                                                                    |\n"  # Operations section
2383            nextDay = ""
2384
2385            info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])]
2386
2387            if len(ops) > 0:
2388                customStat = {
2389                    "opsCount": 0,  # total operations count
2390                    "buyCount": 0,  # buy operations
2391                    "sellCount": 0,  # sell operations
2392                    "buyTotal": {"rub": 0.},  # Buy sums in different currencies
2393                    "sellTotal": {"rub": 0.},  # Sell sums in different currencies
2394                    "payIn": {"rub": 0.},  # Deposit brokerage account
2395                    "payOut": {"rub": 0.},  # Withdrawals
2396                    "divs": {"rub": 0.},  # Dividends income
2397                    "coupons": {"rub": 0.},  # Coupon's income
2398                    "brokerCom": {"rub": 0.},  # Service commissions
2399                    "serviceCom": {"rub": 0.},  # Service commissions
2400                    "marginCom": {"rub": 0.},  # Margin commissions
2401                    "allTaxes": {"rub": 0.},  # Sum of withholding taxes and corrections
2402                }
2403
2404                # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES:
2405                for item in ops:
2406                    if item["state"] == "OPERATION_STATE_EXECUTED":
2407                        payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2408
2409                        # count buy operations:
2410                        if "_BUY" in item["operationType"]:
2411                            customStat["buyCount"] += 1
2412
2413                            if item["payment"]["currency"] in customStat["buyTotal"].keys():
2414                                customStat["buyTotal"][item["payment"]["currency"]] += payment
2415
2416                            else:
2417                                customStat["buyTotal"][item["payment"]["currency"]] = payment
2418
2419                        # count sell operations:
2420                        elif "_SELL" in item["operationType"]:
2421                            customStat["sellCount"] += 1
2422
2423                            if item["payment"]["currency"] in customStat["sellTotal"].keys():
2424                                customStat["sellTotal"][item["payment"]["currency"]] += payment
2425
2426                            else:
2427                                customStat["sellTotal"][item["payment"]["currency"]] = payment
2428
2429                        # count incoming operations:
2430                        elif item["operationType"] in ["OPERATION_TYPE_INPUT"]:
2431                            if item["payment"]["currency"] in customStat["payIn"].keys():
2432                                customStat["payIn"][item["payment"]["currency"]] += payment
2433
2434                            else:
2435                                customStat["payIn"][item["payment"]["currency"]] = payment
2436
2437                        # count withdrawals operations:
2438                        elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]:
2439                            if item["payment"]["currency"] in customStat["payOut"].keys():
2440                                customStat["payOut"][item["payment"]["currency"]] += payment
2441
2442                            else:
2443                                customStat["payOut"][item["payment"]["currency"]] = payment
2444
2445                        # count dividends income:
2446                        elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]:
2447                            if item["payment"]["currency"] in customStat["divs"].keys():
2448                                customStat["divs"][item["payment"]["currency"]] += payment
2449
2450                            else:
2451                                customStat["divs"][item["payment"]["currency"]] = payment
2452
2453                        # count coupon's income:
2454                        elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]:
2455                            if item["payment"]["currency"] in customStat["coupons"].keys():
2456                                customStat["coupons"][item["payment"]["currency"]] += payment
2457
2458                            else:
2459                                customStat["coupons"][item["payment"]["currency"]] = payment
2460
2461                        # count broker commissions:
2462                        elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]:
2463                            if item["payment"]["currency"] in customStat["brokerCom"].keys():
2464                                customStat["brokerCom"][item["payment"]["currency"]] += payment
2465
2466                            else:
2467                                customStat["brokerCom"][item["payment"]["currency"]] = payment
2468
2469                        # count service commissions:
2470                        elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]:
2471                            if item["payment"]["currency"] in customStat["serviceCom"].keys():
2472                                customStat["serviceCom"][item["payment"]["currency"]] += payment
2473
2474                            else:
2475                                customStat["serviceCom"][item["payment"]["currency"]] = payment
2476
2477                        # count margin commissions:
2478                        elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]:
2479                            if item["payment"]["currency"] in customStat["marginCom"].keys():
2480                                customStat["marginCom"][item["payment"]["currency"]] += payment
2481
2482                            else:
2483                                customStat["marginCom"][item["payment"]["currency"]] = payment
2484
2485                        # count withholding taxes:
2486                        elif "_TAX" in item["operationType"]:
2487                            if item["payment"]["currency"] in customStat["allTaxes"].keys():
2488                                customStat["allTaxes"][item["payment"]["currency"]] += payment
2489
2490                            else:
2491                                customStat["allTaxes"][item["payment"]["currency"]] = payment
2492
2493                        else:
2494                            continue
2495
2496                customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"]
2497
2498                # --- view "Actions" lines:
2499                info.extend([
2500                    "| 1                          | 2                             | 3                            | 4                    | 5                      |\n",
2501                    "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n",
2502                    "| **Actions:**               | Trades: {:<21} | Trading volumes:             |                      |                        |\n".format(customStat["opsCount"]),
2503                    "|                            |   Buy: {:<22} | {:<28} |                      |                        |\n".format(
2504                        "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2505                        "  rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else "  —",
2506                    ),
2507                    "|                            |   Sell: {:<21} | {:<28} |                      |                        |\n".format(
2508                        "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2509                        "  rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else "  —",
2510                    ),
2511                ])
2512
2513                opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys()))))
2514                for key in opsKeys:
2515                    if key == "rub":
2516                        continue
2517
2518                    info.extend([
2519                        "|                            |                               | {:<28} |                      |                        |\n".format(
2520                            "  {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0)
2521                        ),
2522                        "|                            |                               | {:<28} |                      |                        |\n".format(
2523                            "  {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0)
2524                        ),
2525                    ])
2526
2527                info.append(splitLine1)
2528
2529                def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str:
2530                    return "|                            | {:<29} | {:<28} | {:<20} | {:<22} |\n".format(
2531                            "  {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else "  —",
2532                            "  {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else "  —",
2533                            "  {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else "  —",
2534                            "  {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else "  —",
2535                    )
2536
2537                # --- view "Payments" lines:
2538                info.append("| **Payments:**              | Deposit on broker account:    | Withdrawals:                 | Dividends income:    | Coupons income:        |\n")
2539                paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys()))))
2540
2541                for key in paymentsKeys:
2542                    info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key))
2543
2544                info.append(splitLine1)
2545
2546                # --- view "Commissions and taxes" lines:
2547                info.append("| **Commissions and taxes:** | Broker commissions:           | Service commissions:         | Margin commissions:  | All taxes/corrections: |\n")
2548                comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys()))))
2549
2550                for key in comKeys:
2551                    info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key))
2552
2553                info.append(splitLine1)
2554
2555                info.extend([
2556                    "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"),
2557                    "| Date and time       | FIGI         | Ticker       | Asset      | Value     | Payment         | Status     | Operation type                                                     |\n",
2558                    "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n",
2559                ])
2560
2561            else:
2562                info.append("Broker returned no operations during this period\n")
2563
2564            # --- view "Operations" section:
2565            for item in ops:
2566                if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]:
2567                    continue
2568
2569                else:
2570                    self.figi = item["figi"] if item["figi"] else ""
2571                    payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2572                    instrument = self.SearchByFIGI(requestPrice=False) if self.figi else {}
2573
2574                    # group of deals during one day:
2575                    if nextDay and item["date"].split("T")[0] != nextDay:
2576                        info.append(splitLine2)
2577                        nextDay = ""
2578
2579                    else:
2580                        nextDay = item["date"].split("T")[0]  # saving current day for splitting
2581
2582                    info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format(
2583                        item["date"].replace("T", " ").replace("Z", "").split(".")[0],
2584                        self.figi if self.figi else "—",
2585                        instrument["ticker"] if instrument else "—",
2586                        instrument["type"] if instrument else "—",
2587                        item["quantity"] if int(item["quantity"]) > 0 else "—",
2588                        "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—",
2589                        TKS_OPERATION_STATES[item["state"]],
2590                        TKS_OPERATION_TYPES[item["operationType"]],
2591                    ))
2592
2593            infoText = "".join(info)
2594
2595            if show:
2596                uLogger.info(infoText)
2597
2598            if self.reportFile:
2599                with open(self.reportFile, "w", encoding="UTF-8") as fH:
2600                    fH.write(infoText)
2601
2602                uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile)))
2603
2604        return ops, customStat
2605
2606    def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame:
2607        """
2608        This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id).
2609
2610        History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`.
2611        Warning! Broker server used ISO UTC time by default.
2612
2613        If `historyFile` is not `None` then method save history to file, otherwise return only pandas dataframe.
2614        Also, `historyFile` used to update history with `onlyMissing` parameter.
2615
2616        See also: `LoadHistory()` and `ShowHistoryChart()` methods.
2617
2618        :param start: see docstring in `GetDatesAsString()` method.
2619        :param end: see docstring in `GetDatesAsString()` method.
2620        :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`,
2621                         `"hour"`, `"day"`. Default: `"hour"`.
2622        :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`.
2623                            False by default. Warning! History appends only from last candle to current time
2624                            with always update last candle!
2625        :param csvSep: separator if csv-file is used, `,` by default.
2626        :param show: if `True` then also prints pandas dataframe to the console.
2627        :return: pandas dataframe with prices history. Headers of columns are defined by default:
2628                 `["date", "time", "open", "high", "low", "close", "volume"]`.
2629        """
2630        strStartDate, strEndDate = GetDatesAsString(start, end)  # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2631        headers = ["date", "time", "open", "high", "low", "close", "volume"]  # sequence and names of column headers
2632        history = None  # empty pandas object for history
2633
2634        if interval not in TKS_CANDLE_INTERVALS.keys():
2635            uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.")
2636            raise Exception("Incorrect value")
2637
2638        if not (self.ticker or self.figi):
2639            uLogger.error("Ticker or FIGI must be defined!")
2640            raise Exception("Ticker or FIGI required")
2641
2642        if self.ticker and not self.figi:
2643            instrumentByTicker = self.SearchByTicker(requestPrice=False, debug=False)
2644            self.figi = instrumentByTicker["figi"] if instrumentByTicker else ""
2645
2646        if self.figi and not self.ticker:
2647            instrumentByFIGI = self.SearchByFIGI(requestPrice=False, debug=False)
2648            self.ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else ""
2649
2650        dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from start time string
2651        dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from end time string
2652        if interval.lower() != "day":
2653            dtEnd += timedelta(seconds=1)  # adds 1 sec for requests, because day end returned by `GetDatesAsString()` as 23:59:59
2654
2655        delta = dtEnd - dtStart  # current UTC time minus last time in file
2656        deltaMinutes = delta.days * 1440 + delta.seconds // 60  # minutes between start and end dates
2657
2658        # calculate history length in candles:
2659        length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1]
2660        if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0:
2661            length += 1  # to avoid fraction time
2662
2663        # calculate data blocks count:
2664        blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2]
2665
2666        uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end))
2667        uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate))
2668        uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval))
2669        uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2]))
2670        uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self.ticker, self.figi))
2671
2672        tempOld = None  # pandas object for old history, if --only-missing key present
2673        lastTime = None  # datetime object of last old candle in file
2674
2675        if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile):
2676            uLogger.debug("--only-missing key present, add only last missing candles...")
2677            uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile)))
2678
2679            tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers)
2680
2681            tempOld["date"] = pd.to_datetime(tempOld["date"])  # load date "as is"
2682            tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d")  # convert date to string
2683            tempOld["time"] = pd.to_datetime(tempOld["time"])  # load time "as is"
2684            tempOld["time"] = tempOld["time"].dt.strftime("%H:%M")  # convert time to string
2685
2686            # get last datetime object from last string in file or minus 1 delta if file is empty:
2687            if len(tempOld) > 0:
2688                lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2689
2690            else:
2691                lastTime = dtEnd - timedelta(days=1)  # history file is empty, so last date set at -1 day
2692
2693            tempOld = tempOld[:-1]  # always remove last old candle because it may be incompletely at the current time
2694
2695        responseJSONs = []  # raw history blocks of data
2696
2697        blockEnd = dtEnd
2698        for item in range(blocks):
2699            tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2]
2700            blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail)
2701
2702            uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format(
2703                item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2704            ))
2705
2706            if blockStart == blockEnd:
2707                uLogger.debug("Skipped this zero-length block...")
2708
2709            else:
2710                # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles
2711                historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles"
2712                self.body = str({
2713                    "figi": self.figi,
2714                    "from": blockStart.strftime(TKS_DATE_TIME_FORMAT),
2715                    "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2716                    "interval": TKS_CANDLE_INTERVALS[interval][0]
2717                })
2718                responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1, debug=False)
2719
2720                if "code" in responseJSON.keys():
2721                    uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks))
2722
2723                else:
2724                    if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1:
2725                        responseJSON["candles"] = responseJSON["candles"][:-1]  # removes last candle for "yesterday" request
2726
2727                    responseJSONs = responseJSON["candles"] + responseJSONs  # add more old history behind newest dates
2728
2729            blockEnd = blockStart
2730
2731        printCount = len(responseJSONs)  # candles to show in console
2732        if responseJSONs:
2733            tempHistory = pd.DataFrame(
2734                data={
2735                    "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2736                    "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2737                    "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs],
2738                    "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs],
2739                    "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs],
2740                    "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs],
2741                    "volume": [int(item["volume"]) for item in responseJSONs],
2742                },
2743                index=range(len(responseJSONs)),
2744                columns=["date", "time", "open", "high", "low", "close", "volume"],
2745            )
2746            tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d")
2747            tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M")
2748
2749            # append only newest candles to old history if --only-missing key present:
2750            if onlyMissing and tempOld is not None and lastTime is not None:
2751                index = 0  # find start index in tempHistory data:
2752
2753                for i, item in tempHistory.iterrows():
2754                    curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2755
2756                    if curTime == lastTime:
2757                        uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
2758                        index = i
2759                        printCount = index + 1
2760                        break
2761
2762                history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True)
2763
2764            else:
2765                history = tempHistory  # if no `--only-missing` key then load full data from server
2766
2767            uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False)))
2768
2769        if history is not None and not history.empty:
2770            if show:
2771                uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format(
2772                    strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]),
2773                    pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False),
2774                ))
2775
2776        else:
2777            uLogger.warning("Received an empty candles history!")
2778
2779        if self.historyFile is not None:
2780            if history is not None and not history.empty:
2781                history.to_csv(self.historyFile, sep=csvSep, index=False, header=None)
2782                uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self.ticker, self.figi, interval, os.path.abspath(self.historyFile)))
2783
2784            else:
2785                uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile)))
2786
2787        else:
2788            uLogger.debug("--output key is not defined. Parsed history file not saved to file, only pandas dataframe returns.")
2789
2790        return history
2791
2792    def LoadHistory(self, filePath: str) -> pd.DataFrame:
2793        """
2794        Load candles history from csv-file and return pandas dataframe object.
2795
2796        See also: `History()` and `ShowHistoryChart()` methods.
2797
2798        :param filePath: path to csv-file to open.
2799        """
2800        loadedHistory = None  # init candles data object
2801
2802        uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...")
2803
2804        if os.path.exists(filePath):
2805            loadedHistory = self.priceModel.LoadFromFile(filePath)  # load data and get chain of candles as pandas dataframe
2806
2807            tfStr = self.priceModel.FormattedDelta(
2808                self.priceModel.timeframe,
2809                "{days} days {hours}h {minutes}m {seconds}s",
2810            ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta(
2811                self.priceModel.timeframe,
2812                "{hours}h {minutes}m {seconds}s",
2813            )
2814
2815            if loadedHistory is not None and not loadedHistory.empty:
2816                uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format(
2817                    len(loadedHistory),
2818                    tfStr,
2819                    pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)),
2820                )
2821
2822            else:
2823                uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath)))
2824
2825        else:
2826            uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath))
2827
2828        return loadedHistory
2829
2830    def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2831        """
2832        Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
2833
2834        Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart.
2835        Default: `index.html` (both for interact and non-interact candlesticks chart).
2836
2837        See also: `History()` and `LoadHistory()` methods.
2838
2839        :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
2840        :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart.
2841                         See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters
2842                         If False then chain of candlesticks will render as not interactive Google Candlestick chart.
2843                         See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
2844        :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to
2845                              html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file.
2846        """
2847        if isinstance(candles, str):
2848            self.priceModel.prices = self.LoadHistory(filePath=candles)  # load candles chain from file
2849            self.priceModel.ticker = os.path.basename(candles)  # use filename as ticker name in PriceGenerator
2850
2851        elif isinstance(candles, pd.DataFrame):
2852            self.priceModel.prices = candles  # set candles chain from variable
2853            self.priceModel.ticker = self.ticker  # use current TKSBrokerAPI ticker as ticker name in PriceGenerator
2854
2855            if "datetime" not in candles.columns:
2856                self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True)  # PriceGenerator uses "datetime" column with date and time
2857
2858        else:
2859            uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!")
2860            raise Exception("Incorrect value")
2861
2862        self.priceModel.horizon = len(self.priceModel.prices)  # use length of candles data as horizon in PriceGenerator
2863
2864        if interact:
2865            uLogger.debug("Rendering interactive candles chart. Wait, please...")
2866
2867            self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2868
2869        else:
2870            uLogger.debug("Rendering non-interactive candles chart. Wait, please...")
2871
2872            self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2873
2874        uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))
2875
2876    def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2877        """
2878        Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response.
2879        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2880
2881        See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`.
2882
2883        :param operation: string "Buy" or "Sell".
2884        :param lots: volume, integer count of lots >= 1.
2885        :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`.
2886        :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`.
2887        :param expDate: string "Undefined" by default or local date in future,
2888                        it is a string with format `%Y-%m-%d %H:%M:%S`.
2889        :return: JSON with response from broker server.
2890        """
2891        if self.accountId is None or not self.accountId:
2892            uLogger.error("Variable `accountId` must be defined for using this method!")
2893            raise Exception("Account ID required")
2894
2895        if operation is None or not operation or operation not in ("Buy", "Sell"):
2896            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
2897            raise Exception("Incorrect value")
2898
2899        if lots is None or lots < 1:
2900            uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.")
2901            lots = 1
2902
2903        if tp is None or tp < 0:
2904            tp = 0
2905
2906        if sl is None or sl < 0:
2907            sl = 0
2908
2909        if expDate is None or not expDate:
2910            expDate = "Undefined"
2911
2912        if not (self.ticker or self.figi):
2913            uLogger.error("Ticker or FIGI must be defined!")
2914            raise Exception("Ticker or FIGI required")
2915
2916        instrument = self.SearchByTicker(requestPrice=True, debug=False) if self.ticker else self.SearchByFIGI(requestPrice=True, debug=False)
2917        self.ticker = instrument["ticker"]
2918        self.figi = instrument["figi"]
2919
2920        uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self.ticker, self.figi, lots, tp, sl, expDate))
2921
2922        openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
2923        self.body = str({
2924            "figi": self.figi,
2925            "quantity": str(lots),
2926            "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
2927            "accountId": str(self.accountId),
2928            "orderType": "ORDER_TYPE_MARKET",  # see: TKS_ORDER_TYPES
2929        })
2930        response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0, debug=False)
2931
2932        if "orderId" in response.keys():
2933            uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format(
2934                operation, response["orderId"],
2935                self.ticker, self.figi, lots,
2936                NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"],
2937                NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"],
2938                NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"],
2939            ))
2940
2941        else:
2942            uLogger.warning("Not `oK` status received! Market order not created. See full debug log or try again and open order later.")
2943
2944        if tp > 0:
2945            self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate)
2946
2947        if sl > 0:
2948            self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate)
2949
2950        return response
2951
2952    def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2953        """
2954        More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response.
2955        If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter.
2956
2957        See also: `Order()` and `Trade()` docstrings.
2958
2959        :param lots: volume, integer count of lots >= 1.
2960        :param tp: float > 0, take profit price of stop-order.
2961        :param sl: float > 0, stop loss price of stop-order.
2962        :param expDate: it's a local date in future.
2963                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
2964        :return: JSON with response from broker server.
2965        """
2966        return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)
2967
2968    def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2969        """
2970        More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response.
2971        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2972
2973        See also: `Order()` and `Trade()` docstrings.
2974
2975        :param lots: volume, integer count of lots >= 1.
2976        :param tp: float > 0, take profit price of stop-order.
2977        :param sl: float > 0, stop loss price of stop-order.
2978        :param expDate: it's a local date in the future.
2979                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
2980        :return: JSON with response from broker server.
2981        """
2982        return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)
2983
2984    def CloseTrades(self, tickers: list, portfolio: dict = None) -> None:
2985        """
2986        Close position of given instruments.
2987
2988        :param tickers: tickers list of instruments that must be closed.
2989        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
2990                         This avoids unnecessary downloading data from the server.
2991        """
2992        if not tickers:
2993            uLogger.info("Tickers list is empty, nothing to close.")
2994
2995        else:
2996            if portfolio is None or not portfolio:
2997                portfolio = self.Overview(show=False)
2998
2999            allOpenedTickers = [item["ticker"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]]
3000            uLogger.debug("All opened instruments by it's tickers names: {}".format(allOpenedTickers))
3001
3002            for ticker in tickers:
3003                if ticker not in allOpenedTickers:
3004                    uLogger.warning("Instrument with ticker [{}] not in open positions list!".format(ticker))
3005                    continue
3006
3007                # search open trade info about instrument by ticker:
3008                instrument = {}
3009                for iType in TKS_INSTRUMENTS:
3010                    if instrument:
3011                        break
3012
3013                    for item in portfolio["stat"][iType]:
3014                        if item["ticker"] == ticker:
3015                            instrument = item
3016                            break
3017
3018                if instrument:
3019                    self.ticker = ticker
3020                    self.figi = instrument["figi"]
3021
3022                    uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format(
3023                        self.ticker,
3024                        self.figi,
3025                        int(instrument["volume"]),
3026                        ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "",
3027                    ))
3028
3029                    tradeLots = abs(instrument["lots"]) - instrument["blocked"]  # available volumes in lots for close operation
3030
3031                    if tradeLots > 0:
3032                        if instrument["blocked"] > 0:
3033                            uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format(
3034                                instrument["blocked"],
3035                                self.ticker,
3036                                tradeLots,
3037                            ))
3038
3039                        # if direction is "Long" then we need sell, if direction is "Short" then we need buy:
3040                        self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots)
3041
3042                    else:
3043                        uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self.ticker))
3044
3045    def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3046        """
3047        Close all positions of given instruments with defined type.
3048
3049        :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
3050        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3051                         This avoids unnecessary downloading data from the server.
3052        """
3053        if iType not in TKS_INSTRUMENTS:
3054            uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType))
3055
3056        else:
3057            if portfolio is None or not portfolio:
3058                portfolio = self.Overview(show=False)
3059
3060            tickers = [item["ticker"] for item in portfolio["stat"][iType]]
3061            uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers))
3062
3063            if tickers and portfolio:
3064                self.CloseTrades(tickers, portfolio)
3065
3066            else:
3067                uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))
3068
3069    def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3070        """
3071        Universal method to create market or limit orders with all available parameters for current `accountId`.
3072        See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`.
3073
3074        If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above
3075        current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
3076
3077        Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell"
3078        then broker immediately open market order as you can do simple --buy or --sell operations!
3079
3080        If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell".
3081        When current price will go up or down to target price value then broker opens a limit order.
3082        Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
3083
3084        Only one attempt and no retry for opens order. If network issue occurred you can create new request.
3085
3086        :param operation: string "Buy" or "Sell".
3087        :param orderType: string "Limit" or "Stop".
3088        :param lots: volume, integer count of lots >= 1.
3089        :param targetPrice: target price > 0. This is open trade price for limit order.
3090        :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice.
3091                           Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
3092        :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types
3093                         "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3094                         Stop loss order always executed by market price.
3095        :param expDate: string "Undefined" by default or local date in future.
3096                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3097                        This date is converting to UTC format for server. This parameter only makes sense for stop-order.
3098                        A limit order has no expiration date, it lasts until the end of the trading day.
3099        :return: JSON with response from broker server.
3100        """
3101        if self.accountId is None or not self.accountId:
3102            uLogger.error("Variable `accountId` must be defined for using this method!")
3103            raise Exception("Account ID required")
3104
3105        if operation is None or not operation or operation not in ("Buy", "Sell"):
3106            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
3107            raise Exception("Incorrect value")
3108
3109        if orderType is None or not orderType or orderType not in ("Limit", "Stop"):
3110            uLogger.error("You must define order type only one of them: `Limit` or `Stop`!")
3111            raise Exception("Incorrect value")
3112
3113        if lots is None or lots < 1:
3114            uLogger.error("You must define trade volume > 0: integer count of lots!")
3115            raise Exception("Incorrect value")
3116
3117        if targetPrice is None or targetPrice <= 0:
3118            uLogger.error("Target price for limit-order must be greater than 0!")
3119            raise Exception("Incorrect value")
3120
3121        if limitPrice is None or limitPrice <= 0:
3122            limitPrice = targetPrice
3123
3124        if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"):
3125            stopType = "Limit"
3126
3127        if expDate is None or not expDate:
3128            expDate = "Undefined"
3129
3130        if not (self.ticker or self.figi):
3131            uLogger.error("Tocker or FIGI must be defined!")
3132            raise Exception("Ticker or FIGI required")
3133
3134        response = {}
3135        instrument = self.SearchByTicker(requestPrice=True, debug=False) if self.ticker else self.SearchByFIGI(requestPrice=True, debug=False)
3136        self.ticker = instrument["ticker"]
3137        self.figi = instrument["figi"]
3138
3139        if orderType == "Limit":
3140            uLogger.debug(
3141                "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format(
3142                    self.ticker, self.figi,
3143                    operation, lots, targetPrice, instrument["currency"],
3144                ))
3145
3146            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3147            self.body = str({
3148                "figi": self.figi,
3149                "quantity": str(lots),
3150                "price": FloatToNano(targetPrice),
3151                "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3152                "accountId": str(self.accountId),
3153                "orderType": "ORDER_TYPE_LIMIT",  # see: TKS_ORDER_TYPES
3154            })
3155            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0, debug=False)
3156
3157            if "orderId" in response.keys():
3158                uLogger.info(
3159                    "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format(
3160                        response["orderId"],
3161                        self.ticker, self.figi,
3162                        operation, lots, targetPrice, instrument["currency"],
3163                    ))
3164
3165                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3166                    if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]:
3167                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format(
3168                            targetPrice, instrument["currency"],
3169                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3170                        ))
3171
3172                    if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]:
3173                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format(
3174                            targetPrice, instrument["currency"],
3175                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3176                        ))
3177
3178            else:
3179                uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log or try again and open order later.")
3180
3181        if orderType == "Stop":
3182            uLogger.debug(
3183                "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format(
3184                    self.ticker, self.figi,
3185                    operation, lots,
3186                    targetPrice, instrument["currency"],
3187                    limitPrice, instrument["currency"],
3188                    stopType, expDate,
3189                ))
3190
3191            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder"
3192            expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT)
3193            stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT"
3194
3195            body = {
3196                "figi": self.figi,
3197                "quantity": str(lots),
3198                "price": FloatToNano(limitPrice),
3199                "stopPrice": FloatToNano(targetPrice),
3200                "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL",  # see: TKS_STOP_ORDER_DIRECTIONS
3201                "accountId": str(self.accountId),
3202                "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL",  # see: TKS_STOP_ORDER_EXPIRATION_TYPES
3203                "stopOrderType": stopOrderType,  # see: TKS_STOP_ORDER_TYPES
3204            }
3205
3206            if expDateUTC:
3207                body["expireDate"] = expDateUTC
3208
3209            self.body = str(body)
3210            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0, debug=False)
3211
3212            if "stopOrderId" in response.keys():
3213                uLogger.info(
3214                    "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format(
3215                        response["stopOrderId"],
3216                        self.ticker, self.figi,
3217                        operation, lots,
3218                        targetPrice, instrument["currency"],
3219                        limitPrice, instrument["currency"],
3220                        TKS_STOP_ORDER_TYPES[stopOrderType],
3221                        datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"],
3222                    ))
3223
3224                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3225                    if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3226                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3227                            targetPrice, instrument["currency"],
3228                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3229                        ))
3230
3231                    if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3232                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3233                            targetPrice, instrument["currency"],
3234                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3235                        ))
3236
3237            else:
3238                uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log or try again and open order later.")
3239
3240        return response
3241
3242    def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3243        """
3244        Create pending `Buy` limit-order (below current price). You must specify only 2 parameters:
3245        `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then
3246        broker immediately open `Buy` market order, such as if you do simple `--buy` operation!
3247        See also: `Order()` docstring.
3248
3249        :param lots: volume, integer count of lots >= 1.
3250        :param targetPrice: target price > 0. This is open trade price for limit order.
3251        :return: JSON with response from broker server.
3252        """
3253        return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)
3254
3255    def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3256        """
3257        Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order.
3258        In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3259        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3260        target price value then broker opens a limit order. See also: `Order()` docstring.
3261
3262        :param lots: volume, integer count of lots >= 1.
3263        :param targetPrice: target price > 0. This is trigger price for buy stop-order.
3264        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3265                           with price equal to limitPrice, when current price goes to target price of buy stop-order.
3266        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3267                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3268        :param expDate: string "Undefined" by default or local date in future.
3269                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3270                        This date is converting to UTC format for server.
3271        :return: JSON with response from broker server.
3272        """
3273        return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3274
3275    def SellLimit(self, lots: int, targetPrice: float) -> dict:
3276        """
3277        Create pending `Sell` limit-order (above current price). You must specify only 2 parameters:
3278        `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then
3279        broker immediately open `Sell` market order, such as if you do simple `--sell` operation!
3280        See also: `Order()` docstring.
3281
3282        :param lots: volume, integer count of lots >= 1.
3283        :param targetPrice: target price > 0. This is open trade price for limit order.
3284        :return: JSON with response from broker server.
3285        """
3286        return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)
3287
3288    def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3289        """
3290        Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order.
3291        In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3292        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3293        target price value then broker opens a limit order. See also: `Order()` docstring.
3294
3295        :param lots: volume, integer count of lots >= 1.
3296        :param targetPrice: target price > 0. This is trigger price for sell stop-order.
3297        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3298                           with price equal to limitPrice, when current price goes to target price of sell stop-order.
3299        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3300                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3301        :param expDate: string "Undefined" by default or local date in future.
3302                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3303                        This date is converting to UTC format for server.
3304        :return: JSON with response from broker server.
3305        """
3306        return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3307
3308    def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3309        """
3310        Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`.
3311
3312        :param orderIDs: list of integers with `orderId` or `stopOrderId`.
3313        :param allOrdersIDs: pre-received lists of all active pending orders.
3314                             This avoids unnecessary downloading data from the server.
3315        :param allStopOrdersIDs: pre-received lists of all active stop orders.
3316        """
3317        if self.accountId is None or not self.accountId:
3318            uLogger.error("Variable `accountId` must be defined for using this method!")
3319            raise Exception("Account ID required")
3320
3321        if orderIDs:
3322            if allOrdersIDs is None or not allOrdersIDs:
3323                rawOrders = self.RequestPendingOrders()
3324                allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending orders ID
3325
3326            if allStopOrdersIDs is None or not allStopOrdersIDs:
3327                rawStopOrders = self.RequestStopOrders()
3328                allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3329
3330            for orderID in orderIDs:
3331                idInPendingOrders = orderID in allOrdersIDs
3332                idInStopOrders = orderID in allStopOrdersIDs
3333
3334                if not (idInPendingOrders or idInStopOrders):
3335                    uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID))
3336                    continue
3337
3338                else:
3339                    if idInPendingOrders:
3340                        uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID))
3341
3342                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder
3343                        self.body = str({"accountId": self.accountId, "orderId": orderID})
3344                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder"
3345                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3346
3347                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3348                            uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3349                            uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID))
3350
3351                        else:
3352                            uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID))
3353
3354                    elif idInStopOrders:
3355                        uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID))
3356
3357                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder
3358                        self.body = str({"accountId": self.accountId, "stopOrderId": orderID})
3359                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder"
3360                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3361
3362                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3363                            uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3364                            uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID))
3365
3366                        else:
3367                            uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID))
3368
3369                    else:
3370                        continue
3371
3372    def CloseAllOrders(self) -> None:
3373        """
3374        Gets a list of open pending and stop orders and cancel it all.
3375        """
3376        rawOrders = self.RequestPendingOrders()
3377        allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending orders ID
3378        lenOrders = len(allOrdersIDs)
3379
3380        rawStopOrders = self.RequestStopOrders()
3381        allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3382        lenSOrders = len(allStopOrdersIDs)
3383
3384        if lenOrders > 0 or lenSOrders > 0:
3385            uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders))
3386
3387            self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs)
3388
3389        else:
3390            uLogger.info("Orders not found, nothing to cancel.")
3391
3392    def CloseAll(self, *args) -> None:
3393        """
3394        Close all available (not blocked) opened trades and orders.
3395
3396        Also, you can select one or more keywords case-insensitive:
3397        `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type.
3398
3399        Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods.
3400        """
3401        overview = self.Overview(show=False)  # get all open trades info
3402
3403        if len(args) == 0:
3404            uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...")
3405            self.CloseAllOrders()  # close all pending and stop orders
3406
3407            for iType in TKS_INSTRUMENTS:
3408                if iType != "Currencies":
3409                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3410
3411        else:
3412            uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args)))
3413            lowerArgs = [x.lower() for x in args]
3414
3415            if "orders" in lowerArgs:
3416                self.CloseAllOrders()  # close all pending and stop orders
3417
3418            for iType in TKS_INSTRUMENTS:
3419                if iType.lower() in lowerArgs and iType != "Currencies":
3420                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3421
3422    @staticmethod
3423    def ParseOrderParameters(operation, **inputParameters):
3424        """
3425        Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
3426
3427        :param operation: string "Buy" or "Sell".
3428        :param inputParameters: this is dict of strings that looks like this
3429               `{"lots": "L_int,...", "prices": "P_float,..."}` where
3430               "lots" key: one or more lot values (integer numbers) to open with every limit-order
3431               "prices" key: one or more prices to open limit-orders
3432               Counts of values in lots and prices lists must be equals!
3433        :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]`
3434        """
3435        # TODO: update order grid work with api v2
3436        pass
3437        # uLogger.debug("Input parameters: {}".format(inputParameters))
3438        #
3439        # if operation is None or not operation or operation not in ("Buy", "Sell"):
3440        #     uLogger.error("You must define operation type: 'Buy' or 'Sell'!")
3441        #     raise Exception("Incorrect value")
3442        #
3443        # if "l" in inputParameters.keys():
3444        #     inputParameters["lots"] = inputParameters.pop("l")
3445        #
3446        # if "p" in inputParameters.keys():
3447        #     inputParameters["prices"] = inputParameters.pop("p")
3448        #
3449        # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys():
3450        #     uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!")
3451        #     raise Exception("Incorrect value")
3452        #
3453        # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")]
3454        # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")]
3455        #
3456        # if len(lots) != len(prices):
3457        #     uLogger.error("'lots' and 'prices' lists must have equal length of values!")
3458        #     raise Exception("Incorrect value")
3459        #
3460        # uLogger.debug("Extracted parameters for orders:")
3461        # uLogger.debug("lots = {}".format(lots))
3462        # uLogger.debug("prices = {}".format(prices))
3463        #
3464        # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...]
3465        # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))]
3466        # uLogger.debug("Order parameters: {}".format(result))
3467        #
3468        # return result
3469
3470    def IsInPortfolio(self, portfolio: dict = None) -> bool:
3471        """
3472        Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`.
3473
3474        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3475        :return: `True` if portfolio contains open position with given instrument, `False` otherwise.
3476        """
3477        result = False
3478        msg = "Instrument not defined!"
3479
3480        if portfolio is None or not portfolio:
3481            portfolio = self.Overview(show=False)
3482
3483        if self.ticker:
3484            uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker))
3485            msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker)
3486
3487            for iType in TKS_INSTRUMENTS:
3488                for instrument in portfolio["stat"][iType]:
3489                    if instrument["ticker"] == self.ticker:
3490                        result = True
3491                        msg = "Instrument with ticker [{}] is present in open positions".format(self.ticker)
3492                        break
3493
3494        elif self.figi:
3495            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi))
3496            msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi)
3497
3498            for iType in TKS_INSTRUMENTS:
3499                for instrument in portfolio["stat"][iType]:
3500                    if instrument["figi"] == self.figi:
3501                        result = True
3502                        msg = "Instrument with FIGI [{}] is present in open positions".format(self.figi)
3503                        break
3504
3505        else:
3506            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3507
3508        uLogger.debug(msg)
3509
3510        return result
3511
3512    def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3513        """
3514        Returns instrument is in the user's portfolio if it presents there.
3515        Instrument must be defined by `ticker` (highly priority) or `figi`.
3516
3517        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3518        :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise.
3519        """
3520        result = None
3521        msg = "Instrument not defined!"
3522
3523        if portfolio is None or not portfolio:
3524            portfolio = self.Overview(show=False)
3525
3526        if self.ticker:
3527            uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker))
3528            msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker)
3529
3530            for iType in TKS_INSTRUMENTS:
3531                for instrument in portfolio["stat"][iType]:
3532                    if instrument["ticker"] == self.ticker:
3533                        result = instrument
3534                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self.ticker, instrument["figi"])
3535                        break
3536
3537        elif self.figi:
3538            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi))
3539            msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi)
3540
3541            for iType in TKS_INSTRUMENTS:
3542                for instrument in portfolio["stat"][iType]:
3543                    if instrument["figi"] == self.figi:
3544                        result = instrument
3545                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self.figi)
3546                        break
3547
3548        else:
3549            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3550
3551        uLogger.debug(msg)
3552
3553        return result
3554
3555    def RequestLimits(self) -> dict:
3556        """
3557        Method for obtaining the available funds for withdrawal for current `accountId`.
3558
3559        See also:
3560        - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
3561        - `OverviewLimits()` method
3562
3563        :return: dict with raw data from server that contains free funds for withdrawal. Example of dict:
3564                 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`.
3565                 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency
3566                 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures.
3567        """
3568        if self.accountId is None or not self.accountId:
3569            uLogger.error("Variable `accountId` must be defined for using this method!")
3570            raise Exception("Account ID required")
3571
3572        uLogger.debug("Requesting current available funds for withdrawal. Wait, please...")
3573
3574        self.body = str({"accountId": self.accountId})
3575        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits"
3576        rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3577
3578        uLogger.debug("Records about available funds for withdrawal successfully received")
3579
3580        return rawLimits
3581
3582    def OverviewLimits(self, show: bool = False) -> dict:
3583        """
3584        Method for parsing and show table with available funds for withdrawal for current `accountId`.
3585
3586        See also: `RequestLimits()`.
3587
3588        :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log.
3589        :return: dict with raw parsed data from server and some calculated statistics about it.
3590        """
3591        if self.accountId is None or not self.accountId:
3592            uLogger.error("Variable `accountId` must be defined for using this method!")
3593            raise Exception("Account ID required")
3594
3595        rawLimits = self.RequestLimits()  # raw response with current available funds for withdrawal
3596
3597        view = {
3598            "rawLimits": rawLimits,
3599            "limits": {  # parsed data for every currency:
3600                "money": {  # this is an array of portfolio currency positions
3601                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"]
3602                },
3603                "blocked": {  # this is an array of blocked currency
3604                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"]
3605                },
3606                "blockedGuarantee": {  # this is locked money under collateral for futures
3607                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"]
3608                },
3609            },
3610        }
3611
3612        # --- Prepare text table with limits in human-readable format:
3613        if show:
3614            info = [
3615                "# Withdrawal limits\n\n",
3616                "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
3617                "* **Account ID:** [{}]\n".format(self.accountId),
3618                "\n| Currencies | Total         | Available for withdrawal | Blocked for trade | Futures guarantee |\n",
3619                "|------------|---------------|--------------------------|-------------------|-------------------|\n",
3620            ]
3621
3622            for curr in view["limits"]["money"].keys():
3623                blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0
3624                blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0
3625                availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee)
3626
3627                infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format(
3628                    "[{}]".format(curr),
3629                    "{:.2f}".format(view["limits"]["money"][curr]),
3630                    "{:.2f}".format(availableMoney),
3631                    "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—",
3632                    "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—",
3633                )
3634
3635                if curr == "rub":
3636                    info.insert(5, infoStr)  # insert at first position in table and after headers
3637
3638                else:
3639                    info.append(infoStr)
3640
3641            infoText = "".join(info)
3642
3643            uLogger.info(infoText)
3644
3645            if self.withdrawalLimitsFile:
3646                with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH:
3647                    fH.write(infoText)
3648
3649                uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile)))
3650
3651        return view
3652
3653    def RequestAccounts(self) -> dict:
3654        """
3655        Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`.
3656
3657        See also:
3658        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
3659        - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
3660        - `OverviewUserInfo()` method
3661
3662        :return: dict with raw data from server that contains accounts info. Example of dict:
3663                 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account",
3664                   "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z",
3665                   "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`.
3666                 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now.
3667        """
3668        uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...")
3669
3670        self.body = str({})
3671        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts"
3672        rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST")
3673
3674        uLogger.debug("Records about available accounts successfully received")
3675
3676        return rawAccounts
3677
3678    def RequestUserInfo(self) -> dict:
3679        """
3680        Method for requesting common user's information.
3681
3682        See also:
3683        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
3684        - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
3685        - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with
3686        - `OverviewUserInfo()` method
3687
3688        :return: dict with raw data from server that contains user's information. Example of dict:
3689                 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage",
3690                   "russian_shares", "structured_income_bonds"], "tariff": "premium"}`.
3691        """
3692        uLogger.debug("Requesting common user's information. Wait, please...")
3693
3694        self.body = str({})
3695        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo"
3696        rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST")
3697
3698        uLogger.debug("Records about current user successfully received")
3699
3700        return rawUserInfo
3701
3702    def RequestMarginStatus(self, accountId: str = None) -> dict:
3703        """
3704        Method for requesting margin calculation for defined account ID.
3705
3706        See also:
3707        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
3708        - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
3709        - `OverviewUserInfo()` method
3710
3711        :param accountId: string with numeric account ID. If `None`, then used class field `accountId`.
3712        :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict.
3713                 Example of responses:
3714                 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`.
3715                 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000},
3716                                    "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000},
3717                                    "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000},
3718                                    "fundsSufficiencyLevel": {"units": "1", "nano": 280000000},
3719                                    "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`.
3720        """
3721        if accountId is None or not accountId:
3722            if self.accountId is None or not self.accountId:
3723                uLogger.error("Variable `accountId` must be defined for using this method!")
3724                raise Exception("Account ID required")
3725
3726            else:
3727                accountId = self.accountId  # use `self.accountId` (main ID) by default
3728
3729        uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId))
3730
3731        self.body = str({"accountId": accountId})
3732        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes"
3733        rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST")
3734
3735        if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}:
3736            uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId))
3737            rawMargin = {}
3738
3739        else:
3740            uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId))
3741
3742        return rawMargin
3743
3744    def RequestTariffLimits(self) -> dict:
3745        """
3746        Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`.
3747
3748        See also:
3749        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
3750        - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
3751        - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
3752        - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
3753        - `OverviewUserInfo()` method
3754
3755        :return: dict with raw data from server that contains limits of current tariff. Example of dict:
3756                 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...],
3757                   "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`.
3758        """
3759        uLogger.debug("Requesting limits of current tariff. Wait, please...")
3760
3761        self.body = str({})
3762        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff"
3763        rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3764
3765        uLogger.debug("Records with limits of current tariff successfully received")
3766
3767        return rawTariffLimits
3768
3769    def RequestBondCoupons(self, iJSON: dict) -> dict:
3770        """
3771        Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
3772        then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`.
3773        All dates are in UTC timezone.
3774
3775        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons
3776        Documentation:
3777        - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
3778        - response: https://tinkoff.github.io/investAPI/instruments/#coupon
3779
3780        See also: `ExtendBondsData()`.
3781
3782        :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self.ticker]`
3783                      If raw iJSON is not data of bond then server returns an error [400] with message:
3784                      `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`.
3785        :return: dictionary with bond payment calendar. Response example
3786                 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12",
3787                   "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000},
3788                   "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z",
3789                   "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}`
3790        """
3791        if iJSON["figi"] is None or not iJSON["figi"]:
3792            uLogger.error("FIGI must be defined for using this method!")
3793            raise Exception("FIGI required")
3794
3795        startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z"
3796        endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z"
3797
3798        uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format(
3799            "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "",
3800            self.figi,
3801            startDate,
3802            endDate,
3803        ))
3804
3805        self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate})
3806        calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons"
3807        calendar = self.SendAPIRequest(calendarURL, reqType="POST", debug=False)
3808
3809        if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}:
3810            uLogger.warning("Instrument type is not bond!")
3811
3812        else:
3813            uLogger.debug("Records about bond payment calendar successfully received")
3814
3815        return calendar
3816
3817    def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame:
3818        """
3819        Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider
3820        pandas dataframe with more information about bonds: main info, current prices, bond payment calendar,
3821        coupon yields, current yields and some statistics etc.
3822
3823        WARNING! This is too long operation if a lot of bonds requested from broker server.
3824
3825        See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`.
3826
3827        :param instruments: list of strings with tickers or FIGIs.
3828        :param xlsx: if True then also exports pandas dataframe to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`,
3829                     for further used by data scientists or stock analytics.
3830        :return: wider pandas dataframe with more full and calculated data about bonds, than raw response from broker.
3831                 In XLSX-file and pandas dataframe fields mean:
3832                 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond
3833                 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
3834        """
3835        if instruments is None or not instruments:
3836            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
3837            raise Exception("Ticker or FIGI required")
3838
3839        if isinstance(instruments, str):
3840            instruments = [instruments]
3841
3842        uniqueInstruments = self.GetUniqueFIGIs(instruments)
3843
3844        uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...")
3845
3846        iCount = len(uniqueInstruments)
3847        tooLong = iCount >= 20
3848        if tooLong:
3849            uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...")
3850
3851        bonds = None
3852        for i, self.figi in enumerate(uniqueInstruments):
3853            instrument = self.SearchByFIGI(requestPrice=False)  # raw data about instrument from server
3854
3855            if "type" in instrument.keys() and instrument["type"] == "Bonds":
3856                # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond
3857                rawBond = self.SearchByFIGI(requestPrice=True)
3858
3859                # Widen raw data with UTC current time (iData["actualDateTime"]):
3860                actualDate = datetime.now(tzutc())
3861                iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond
3862
3863                # Widen raw data with bond payment calendar (iData["rawCalendar"]):
3864                iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)}
3865
3866                # Replace some values with human-readable:
3867                iData["nominalCurrency"] = iData["nominal"]["currency"]
3868                iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"])
3869                iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"])
3870                iData["aciCurrency"] = iData["aciValue"]["currency"]
3871                iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"])
3872                iData["issueSize"] = int(iData["issueSize"])
3873                iData["issueSizePlan"] = int(iData["issueSizePlan"])
3874                iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]]
3875                iData["step"] = iData["step"] if "step" in iData.keys() else 0
3876                iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]]
3877                iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0
3878                iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0
3879                iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0
3880                iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0
3881                iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0
3882                iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0
3883
3884                # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date):
3885                iData["limitUpPercent"] = iData["currentPrice"]["limitUp"]  # max price on current day in percents of nominal
3886                iData["limitDownPercent"] = iData["currentPrice"]["limitDown"]  # min price on current day in percents of nominal
3887                iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"]  # last price on market in percents of nominal
3888                iData["closePricePercent"] = iData["currentPrice"]["closePrice"]  # previous day close in percents of nominal
3889                iData["changes"] = iData["currentPrice"]["changes"]  # this is percent of changes between `currentPrice` and `lastPrice`
3890                iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100  # max price on current day is `limitUpPercent` * `nominal`
3891                iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100  # min price on current day is `limitDownPercent` * `nominal`
3892                iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100  # last price on market is `lastPricePercent` * `nominal`
3893                iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100  # previous day close is `closePricePercent` * `nominal`
3894                iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"]  # this is delta between last deal price and last close
3895
3896                # Widen raw data with calendar data from `rawCalendar` values:
3897                calendarData = []
3898                for item in iData["rawCalendar"]["events"]:
3899                    calendarData.append({
3900                        "couponDate": item["couponDate"],
3901                        "couponNumber": int(item["couponNumber"]),
3902                        "fixDate": item["fixDate"] if "fixDate" in item.keys() else "",
3903                        "payCurrency": item["payOneBond"]["currency"],
3904                        "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]),
3905                        "couponType": TKS_COUPON_TYPES[item["couponType"]],
3906                        "couponStartDate": item["couponStartDate"],
3907                        "couponEndDate": item["couponEndDate"],
3908                        "couponPeriod": item["couponPeriod"],
3909                    })
3910
3911                # if maturity date is unknown then uses the latest date in bond payment calendar for it:
3912                if "maturityDate" not in iData.keys():
3913                    iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else ""
3914
3915                # Widen raw data with Coupon Rate.
3916                # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%:
3917                iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData])
3918                iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData])
3919                iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0.
3920
3921                # Widen raw data with Yield to Maturity (YTM) on current date.
3922                # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%:
3923                maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None
3924                iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None
3925                iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate])
3926                iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"]  # sum of all last coupons minus current ACI value
3927                iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0.
3928
3929                iData["calendar"] = calendarData  # adds calendar at the end
3930
3931                # Remove not used data:
3932                iData.pop("uid")
3933                iData.pop("positionUid")
3934                iData.pop("currentPrice")
3935                iData.pop("rawCalendar")
3936
3937                colNames = list(iData.keys())
3938                if bonds is None:
3939                    bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames))
3940
3941                else:
3942                    bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True)
3943
3944            else:
3945                uLogger.warning("Instrument with ticker [{}] and FIGI [{}] is not a bond!".format(instrument["ticker"], instrument["figi"]))
3946
3947            processed = round(100 * (i + 1) / iCount, 1)
3948            if tooLong and processed % 5 == 0:
3949                uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount))
3950
3951            else:
3952                uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount))
3953
3954        bonds.index = bonds["ticker"].tolist()  # replace indexes with ticker names
3955
3956        # Saving bonds from pandas dataframe to XLSX sheet:
3957        if xlsx and self.bondsXLSXFile:
3958            with pd.ExcelWriter(
3959                    path=self.bondsXLSXFile,
3960                    date_format=TKS_DATE_FORMAT,
3961                    datetime_format=TKS_DATE_TIME_FORMAT,
3962                    mode="w",
3963            ) as writer:
3964                bonds.to_excel(
3965                    writer,
3966                    sheet_name="Extended bonds data",
3967                    index=True,
3968                    encoding="UTF-8",
3969                    freeze_panes=(1, 1),
3970                )  # saving as XLSX-file with freeze first row and column as headers
3971
3972            uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile)))
3973
3974        return bonds
3975
3976    def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame:
3977        """
3978        Creates bond payments calendar as pandas dataframe, and also save it to the XLSX-file, `calendar.xlsx` by default.
3979
3980        WARNING! This is too long operation if a lot of bonds requested from broker server.
3981
3982        See also: `ShowBondsCalendar()`, `ExtendBondsData()`.
3983
3984        :param extBonds: pandas dataframe object returns by `ExtendBondsData()` method and contains
3985                        extended information about bonds: main info, current prices, bond payment calendar,
3986                        coupon yields, current yields and some statistics etc.
3987                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
3988        :param xlsx: if True then also exports pandas dataframe to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default,
3989                     for further used by data scientists or stock analytics.
3990        :return: pandas dataframe with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
3991        """
3992        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
3993            extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False)
3994
3995        uLogger.debug("Generating bond payments calendar data. Wait, please...")
3996
3997        colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"]
3998        colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"]
3999        calendar = None
4000        for bond in extBonds.iterrows():
4001            for item in bond[1]["calendar"]:
4002                cData = {
4003                    "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()),
4004                    "couponDate": item["couponDate"],
4005                    "figi": bond[1]["figi"],
4006                    "ticker": bond[1]["ticker"],
4007                    "name": bond[1]["name"],
4008                    "couponNumber": item["couponNumber"],
4009                    "payOneBond": item["payOneBond"],
4010                    "payCurrency": item["payCurrency"],
4011                    "couponType": item["couponType"],
4012                    "couponPeriod": item["couponPeriod"],
4013                    "fixDate": item["fixDate"],
4014                    "couponStartDate": item["couponStartDate"],
4015                    "couponEndDate": item["couponEndDate"],
4016                }
4017
4018                if calendar is None:
4019                    calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID))
4020
4021                else:
4022                    calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True)
4023
4024        calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True)  # sort all payments for all bonds by payment date
4025
4026        # Saving calendar from pandas dataframe to XLSX sheet:
4027        if xlsx:
4028            xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx"
4029
4030            with pd.ExcelWriter(
4031                    path=xlsxCalendarFile,
4032                    date_format=TKS_DATE_FORMAT,
4033                    datetime_format=TKS_DATE_TIME_FORMAT,
4034                    mode="w",
4035            ) as writer:
4036                humanReadable = calendar.copy(deep=True)
4037                humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0])
4038                humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0])
4039                humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0])
4040                humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0])
4041                humanReadable.columns = colNames  # human-readable column names
4042
4043                humanReadable.to_excel(
4044                    writer,
4045                    sheet_name="Bond payments calendar",
4046                    index=False,
4047                    encoding="UTF-8",
4048                    freeze_panes=(1, 2),
4049                )  # saving as XLSX-file with freeze first row and column as headers
4050
4051                del humanReadable  # release df in memory
4052
4053            uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile)))
4054
4055        return calendar
4056
4057    def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str:
4058        """
4059        Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond.
4060        Also, creates Markdown file with calendar data, `calendar.md` by default.
4061
4062        See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`.
4063
4064        :param extBonds: pandas dataframe object returns by `ExtendBondsData()` method and contains
4065                        extended information about bonds: main info, current prices, bond payment calendar,
4066                        coupon yields, current yields and some statistics etc.
4067                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4068        :param show: if `True` then also printing bonds payment calendar to the console,
4069                     otherwise save to file `calendarFile` only. `False` by default.
4070        :return: multilines text in Markdown format with bonds payment calendar as a table.
4071        """
4072        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4073            extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False)
4074
4075        infoText = "# Bond payments calendar\n\n"
4076
4077        calendar = self.CreateBondsCalendar(extBonds, xlsx=True)  # generate pandas dataframe with full calendar data
4078
4079        if not calendar.empty:
4080            splitLine = "|       |                 |              |              |     |               |           |        |                   |\n"
4081
4082            info = [
4083                "| Paid  | Payment date    | FIGI         | Ticker       | No. | Value         | Type      | Period | End registry date |\n",
4084                "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n",
4085            ]
4086
4087            newMonth = False
4088            notOneBond = calendar["figi"].nunique() > 1
4089            for i, bond in enumerate(calendar.iterrows()):
4090                if newMonth and notOneBond:
4091                    info.append(splitLine)
4092
4093                info.append(
4094                    "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format(
4095                        "  √" if bond[1]["paid"] else "  —",
4096                        bond[1]["couponDate"].split("T")[0],
4097                        bond[1]["figi"],
4098                        bond[1]["ticker"],
4099                        bond[1]["couponNumber"],
4100                        "{} {}".format(
4101                            "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."),
4102                            bond[1]["payCurrency"],
4103                        ),
4104                        bond[1]["couponType"],
4105                        bond[1]["couponPeriod"],
4106                        bond[1]["fixDate"].split("T")[0],
4107                    )
4108                )
4109
4110                if i < len(calendar.values) - 1:
4111                    curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4112                    nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4113                    newMonth = False if curDate.month == nextDate.month else True
4114
4115                else:
4116                    newMonth = False
4117
4118            infoText += "".join(info)
4119
4120            if show:
4121                uLogger.info("{}".format(infoText))
4122
4123            if self.calendarFile is not None:
4124                with open(self.calendarFile, "w", encoding="UTF-8") as fH:
4125                    fH.write(infoText)
4126
4127                uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile)))
4128
4129        else:
4130            infoText += "No data\n"
4131
4132        return infoText
4133
4134    def OverviewAccounts(self, show: bool = False) -> dict:
4135        """
4136        Method for parsing and show simple table with all available user accounts.
4137
4138        See also: `RequestAccounts()` and `OverviewUserInfo()` methods.
4139
4140        :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log.
4141        :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict:
4142                 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...},
4143                          "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1",
4144                                                        "status": "Opened and active account", "opened": "2018-05-23 00:00:00",
4145                                                        "closed": "—", "access": "Full access" }, ...}}`
4146        """
4147        rawAccounts = self.RequestAccounts()  # Raw responses with accounts
4148
4149        # This is an array of dict with user accounts, its `accountId`s and some parsed data:
4150        accounts = {
4151            item["id"]: {
4152                "type": TKS_ACCOUNT_TYPES[item["type"]],
4153                "name": item["name"],
4154                "status": TKS_ACCOUNT_STATUSES[item["status"]],
4155                "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4156                "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—",
4157                "access": TKS_ACCESS_LEVELS[item["accessLevel"]],
4158            } for item in rawAccounts["accounts"]
4159        }
4160
4161        # Raw and parsed data with some fields replaced in "stat" section:
4162        view = {
4163            "rawAccounts": rawAccounts,
4164            "stat": accounts,
4165        }
4166
4167        # --- Prepare simple text table with only accounts data in human-readable format:
4168        if show:
4169            info = [
4170                "# User accounts\n\n",
4171                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4172                "| Account ID   | Type                      | Status                    | Name                           |\n",
4173                "|--------------|---------------------------|---------------------------|--------------------------------|\n",
4174            ]
4175
4176            for account in view["stat"].keys():
4177                info.extend([
4178                    "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format(
4179                        account,
4180                        view["stat"][account]["type"],
4181                        view["stat"][account]["status"],
4182                        view["stat"][account]["name"],
4183                    )
4184                ])
4185
4186            infoText = "".join(info)
4187
4188            uLogger.info(infoText)
4189
4190            if self.userAccountsFile:
4191                with open(self.userAccountsFile, "w", encoding="UTF-8") as fH:
4192                    fH.write(infoText)
4193
4194                uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile)))
4195
4196        return view
4197
4198    def OverviewUserInfo(self, show: bool = False) -> dict:
4199        """
4200        Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit).
4201
4202        See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods.
4203
4204        :param show: if `False` then only dictionary returns, if `True` then also print user's data to log.
4205        :return: dict with raw parsed data from server and some calculated statistics about it.
4206        """
4207        rawUserInfo = self.RequestUserInfo()  # Raw response with common user info
4208        overviewAccount = self.OverviewAccounts(show=False)  # Raw and parsed accounts data
4209        rawAccounts = overviewAccount["rawAccounts"]  # Raw response with user accounts data
4210        accounts = overviewAccount["stat"]  # Dict with only statistics about user accounts
4211        rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()}  # Raw response with margin calculation for every account ID
4212        rawTariffLimits = self.RequestTariffLimits()  # Raw response with limits of current tariff
4213
4214        # This is dict with parsed common user data:
4215        userInfo = {
4216            "premium": "Yes" if rawUserInfo["premStatus"] else "No",
4217            "qualified": "Yes" if rawUserInfo["qualStatus"] else "No",
4218            "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]],
4219            "tariff": rawUserInfo["tariff"],
4220        }
4221
4222        # This is an array of dict with parsed margin statuses for every account IDs:
4223        margins = {}
4224        for accountId in accounts.keys():
4225            if rawMargins[accountId]:
4226                margins[accountId] = {
4227                    "currency": rawMargins[accountId]["liquidPortfolio"]["currency"],
4228                    "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]),
4229                    "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]),
4230                    "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]),
4231                    "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]),
4232                    "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]),
4233                }
4234
4235            else:
4236                margins[accountId] = {}  # Server response: margin status is disabled for current accountId
4237
4238        unary = {}  # unary-connection limits
4239        for item in rawTariffLimits["unaryLimits"]:
4240            if item["limitPerMinute"] in unary.keys():
4241                unary[item["limitPerMinute"]].extend(item["methods"])
4242
4243            else:
4244                unary[item["limitPerMinute"]] = item["methods"]
4245
4246        stream = {}  # stream-connection limits
4247        for item in rawTariffLimits["streamLimits"]:
4248            if item["limit"] in stream.keys():
4249                stream[item["limit"]].extend(item["streams"])
4250
4251            else:
4252                stream[item["limit"]] = item["streams"]
4253
4254        # This is dict with parsed limits of current tariff (connections, API methods etc.):
4255        limits = {
4256            "unary": unary,
4257            "stream": stream,
4258        }
4259
4260        # Raw and parsed data as an output result:
4261        view = {
4262            "rawUserInfo": rawUserInfo,
4263            "rawAccounts": rawAccounts,
4264            "rawMargins": rawMargins,
4265            "rawTariffLimits": rawTariffLimits,
4266            "stat": {
4267                "userInfo": userInfo,
4268                "accounts": accounts,
4269                "margins": margins,
4270                "limits": limits,
4271            },
4272        }
4273
4274        # --- Prepare text table with user information in human-readable format:
4275        if show:
4276            info = [
4277                "# Full user information\n\n",
4278                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4279                "## Common information\n\n",
4280                "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]),
4281                "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]),
4282                "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]),
4283                "* **Allowed to work with instruments:**\n{}\n".format("".join(["  - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])),
4284                "\n## User accounts\n\n",
4285            ]
4286
4287            for account in view["stat"]["accounts"].keys():
4288                info.extend([
4289                    "### ID: [{}]\n\n".format(account),
4290                    "| Parameters           | Values                                                       |\n",
4291                    "|----------------------|--------------------------------------------------------------|\n",
4292                    "| Account type:        | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]),
4293                    "| Account name:        | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]),
4294                    "| Account status:      | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]),
4295                    "| Access level:        | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]),
4296                    "| Date opened:         | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]),
4297                    "| Date closed:         | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]),
4298                ])
4299
4300                if margins[account]:
4301                    info.extend([
4302                        "| Margin status:       | Enabled                                                      |\n",
4303                        "| - Liquid portfolio:  | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])),
4304                        "| - Margin starting:   | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])),
4305                        "| - Margin minimum:    | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])),
4306                        "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)),
4307                        "| - Missing funds:     | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])),
4308                    ])
4309
4310                else:
4311                    info.append("| Margin status:       | Disabled                                                     |\n\n")
4312
4313            info.extend([
4314                "\n## Current user tariff limits\n",
4315                "\nSee also:\n",
4316                "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n",
4317                "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n",
4318                "  - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n",
4319                "  - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n",
4320                "\n### Unary limits\n",
4321            ])
4322
4323            if unary:
4324                for key, values in sorted(unary.items()):
4325                    info.append("\n* Max requests per minute: {}\n".format(key))
4326
4327                    for value in values:
4328                        info.append("  - {}\n".format(value))
4329
4330            else:
4331                info.append("\nNot available\n")
4332
4333            info.append("\n### Stream limits\n")
4334
4335            if stream:
4336                for key, values in sorted(stream.items()):
4337                    info.append("\n* Max stream connections: {}\n".format(key))
4338
4339                    for value in values:
4340                        info.append("  - {}\n".format(value))
4341
4342            else:
4343                info.append("\nNot available\n")
4344
4345            infoText = "".join(info)
4346
4347            uLogger.info(infoText)
4348
4349            if self.userInfoFile:
4350                with open(self.userInfoFile, "w", encoding="UTF-8") as fH:
4351                    fH.write(infoText)
4352
4353                uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile)))
4354
4355        return view

This class implements methods to work with Tinkoff broker server.

Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/

About token: https://tinkoff.github.io/investAPI/token/

TinkoffBrokerServer( token: str, accountId: str = None, useCache: bool = True, defaultCache: str = 'dump.json')
196    def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None:
197        """
198        Main class init.
199
200        :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`.
201        :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
202                          Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.
203        :param useCache: use default cache file with raw data to use instead of `iList`.
204                         True by default. Cache is auto-update if new day has come.
205                         If you don't want to use cache and always updates raw data then set `useCache=False`.
206        :param defaultCache: path to default cache file. `dump.json` by default.
207        """
208        if token is None or not token:
209            try:
210                self.token = r"{}".format(os.environ["TKS_API_TOKEN"])
211                uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/")
212
213            except KeyError:
214                uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/")
215                raise Exception("Token required")
216
217        else:
218            self.token = token  # highly priority than environment variable 'TKS_API_TOKEN'
219            uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`")
220
221        if accountId is None or not accountId:
222            try:
223                self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"])
224                uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId))
225
226            except KeyError:
227                uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).")
228
229        else:
230            self.accountId = accountId  # highly priority than environment variable 'TKS_ACCOUNT_ID'
231            uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId))
232
233        self.version = __version__  # duplicate here used TKSBrokerAPI main version
234        """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
235
236        Latest version: https://pypi.org/project/tksbrokerapi/
237        """
238
239        self.aliases = TKS_TICKER_ALIASES
240        """Some aliases instead official tickers.
241
242        See also: `TKSEnums.TKS_TICKER_ALIASES`
243        """
244
245        self.aliasesKeys = self.aliases.keys()  # re-calc only first time at class init
246
247        self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED  # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there
248
249        self.ticker = ""
250        """String with ticker, e.g. `GOOGL`. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
251
252        See also: `SearchByTicker()`, `SearchInstruments()`.
253        """
254
255        self.figi = ""
256        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`.
257
258        See also: `SearchByFIGI()`, `SearchInstruments()`.
259        """
260
261        self.depth = 1
262        """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI.
263
264        See also: `GetCurrentPrices()`.
265        """
266
267        self.server = r"https://invest-public-api.tinkoff.ru/rest"
268        """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
269
270        See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`.
271        """
272
273        uLogger.debug("Broker API server: {}".format(self.server))
274
275        self.timeout = 15
276        """Server operations timeout in seconds. Default: `15`.
277
278        See also: `SendAPIRequest()`.
279        """
280
281        self.headers = {
282            "Content-Type": "application/json",
283            "accept": "application/json",
284            "Authorization": "Bearer {}".format(self.token),
285            "x-app-name": "Tim55667757.TKSBrokerAPI",
286        }
287        """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`.
288
289        See also: `SendAPIRequest()`.
290        """
291
292        self.body = None
293        """Request body which send to broker server. Default: `None`.
294
295        See also: `SendAPIRequest()`.
296        """
297
298        self.historyFile = None
299        """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only pandas dataframe.
300
301        See also: `History()`.
302        """
303
304        self.htmlHistoryFile = "index.html"
305        """Full path to the html file where rendered candles chart stored. Default: `index.html`.
306
307        See also: `ShowHistoryChart()`.
308        """
309
310        self.instrumentsFile = "instruments.md"
311        """Filename where full available to user instruments list will be saved. Default: `instruments.md`.
312
313        See also: `ShowInstrumentsInfo()`.
314        """
315
316        self.searchResultsFile = "search-results.md"
317        """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`.
318
319        See also: `SearchInstruments()`.
320        """
321
322        self.pricesFile = "prices.md"
323        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
324
325        See also: `GetListOfPrices()`.
326        """
327
328        self.infoFile = "info.md"
329        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
330
331        See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`.
332        """
333
334        self.bondsXLSXFile = "ext-bonds.xlsx"
335        """Filename where wider pandas dataframe with more information about bonds: main info, current prices, 
336        bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`.
337
338        See also: `ExtendBondsData()`.
339        """
340
341        self.calendarFile = "calendar.md"
342        """Filename where bonds payment calendar will be saved. Default: `calendar.md`.
343        
344        Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`.
345
346        See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`.
347        """
348
349        self.overviewFile = "overview.md"
350        """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`.
351
352        See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`.
353        """
354
355        self.overviewDigestFile = "overview-digest.md"
356        """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`.
357
358        See also: `Overview()` with parameter `details="digest"`.
359        """
360
361        self.overviewPositionsFile = "overview-positions.md"
362        """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`.
363
364        See also: `Overview()` with parameter `details="positions"`.
365        """
366
367        self.overviewOrdersFile = "overview-orders.md"
368        """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`.
369
370        See also: `Overview()` with parameter `details="orders"`.
371        """
372
373        self.overviewAnalyticsFile = "overview-analytics.md"
374        """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`.
375
376        See also: `Overview()` with parameter `details="analytics"`.
377        """
378
379        self.reportFile = "deals.md"
380        """Filename where history of deals and trade statistics will be saved. Default: `deals.md`.
381
382        See also: `Deals()`.
383        """
384
385        self.withdrawalLimitsFile = "limits.md"
386        """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`.
387
388        See also: `OverviewLimits()` and `RequestLimits()`.
389        """
390
391        self.userInfoFile = "user-info.md"
392        """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`.
393
394        See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`.
395        """
396
397        self.userAccountsFile = "accounts.md"
398        """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`.
399
400        See also: `OverviewAccounts()`, `RequestAccounts()`.
401        """
402
403        self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache
404        """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`.
405
406        Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`.
407
408        See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`.
409        """
410
411        self.iList = None  # init iList for raw instruments data
412        """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`.
413        
414        See also: `Listing()`, `DumpInstruments()`.
415        """
416
417        # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server:
418        if useCache:
419            if os.path.exists(self.iListDumpFile):
420                dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc())  # dump modification date and time
421                curTime = datetime.now(tzutc())
422
423                if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year):
424                    uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
425
426                    self.DumpInstruments(forceUpdate=True)  # updating self.iList and dump file
427
428                else:
429                    self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8"))  # load iList from dump
430
431                    uLogger.debug("Local cache with raw instruments data is used: [{}]".format(os.path.abspath(self.iListDumpFile)))
432                    uLogger.debug("Dump file was last modified [{}] UTC".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
433
434            else:
435                uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...")
436                self.DumpInstruments(forceUpdate=True)  # updating self.iList and creating default dump file
437
438        else:
439            self.iList = self.Listing()  # request new raw instruments data from broker server
440            self.DumpInstruments(forceUpdate=False)  # save raw instrument's data to default dump file `iListDumpFile`
441
442        self.priceModel = PriceGenerator()  # init PriceGenerator object to work with candles data
443        """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
444
445        See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
446        """

Main class init.

Parameters
  • token: Bearer token for Tinkoff Invest API. It can be set from environment variable TKS_API_TOKEN.
  • accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. Also, this variable can be set from environment variable TKS_ACCOUNT_ID.
  • useCache: use default cache file with raw data to use instead of iList. True by default. Cache is auto-update if new day has come. If you don't want to use cache and always updates raw data then set useCache=False.
  • defaultCache: path to default cache file. dump.json by default.
version

Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.

Latest version: https://pypi.org/project/tksbrokerapi/

aliases

Some aliases instead official tickers.

See also: TKSEnums.TKS_TICKER_ALIASES

ticker

String with ticker, e.g. GOOGL. Use alias for USD000UTSTOM simple as USD, EUR_RUB__TOM as EUR etc. More tickers aliases here: TKSEnums.TKS_TICKER_ALIASES.

See also: SearchByTicker(), SearchInstruments().

figi

String with FIGI, e.g. ticker GOOGL has FIGI BBG009S39JX6.

See also: SearchByFIGI(), SearchInstruments().

depth

Depth of Market (DOM) can be >= 1. Default: 1. It used with --price key to showing DOM with current prices for givens ticker or FIGI.

See also: GetCurrentPrices().

server

Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest

See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and SendAPIRequest().

timeout

Server operations timeout in seconds. Default: 15.

See also: SendAPIRequest().

headers

Headers which send in every request to broker server. Please, do not change it! Default: {"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}.

See also: SendAPIRequest().

body

Request body which send to broker server. Default: None.

See also: SendAPIRequest().

historyFile

Full path to the output file where history candles will be saved or updated. Default: None, it mean that returns only pandas dataframe.

See also: History().

htmlHistoryFile

Full path to the html file where rendered candles chart stored. Default: index.html.

See also: ShowHistoryChart().

instrumentsFile

Filename where full available to user instruments list will be saved. Default: instruments.md.

See also: ShowInstrumentsInfo().

searchResultsFile

Filename with all found instruments searched by part of its ticker, FIGI or name. Default: search-results.md.

See also: SearchInstruments().

pricesFile

Filename where prices of selected instruments will be saved. Default: prices.md.

See also: GetListOfPrices().

infoFile

Filename where prices of selected instruments will be saved. Default: prices.md.

See also: ShowInstrumentsInfo(), RequestBondCoupons() and RequestTradingStatus().

bondsXLSXFile

Filename where wider pandas dataframe with more information about bonds: main info, current prices, bonds payment calendar, some statistics will be stored. Default: ext-bonds.xlsx.

See also: ExtendBondsData().

calendarFile

Filename where bonds payment calendar will be saved. Default: calendar.md.

Pandas dataframe with only bonds payment calendar also will be stored to default file calendar.xlsx.

See also: CreateBondsCalendar(), ShowBondsCalendar(), ShowInstrumentInfo(), RequestBondCoupons() and ExtendBondsData().

overviewFile

Filename where current portfolio, open trades and orders will be saved. Default: overview.md.

See also: Overview(), RequestPortfolio(), RequestPositions(), RequestPendingOrders() and RequestStopOrders().

overviewDigestFile

Filename where short digest of the portfolio status will be saved. Default: overview-digest.md.

See also: Overview() with parameter details="digest".

overviewPositionsFile

Filename where only open positions, without everything else will be saved. Default: overview-positions.md.

See also: Overview() with parameter details="positions".

overviewOrdersFile

Filename where open limits and stop orders will be saved. Default: overview-orders.md.

See also: Overview() with parameter details="orders".

overviewAnalyticsFile

Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: overview-analytics.md.

See also: Overview() with parameter details="analytics".

reportFile

Filename where history of deals and trade statistics will be saved. Default: deals.md.

See also: Deals().

withdrawalLimitsFile

Filename where table of funds available for withdrawal will be saved. Default: limits.md.

See also: OverviewLimits() and RequestLimits().

userInfoFile

Filename where all available user's data (accountIds, common user information, margin status and tariff connections limit) will be saved. Default: user-info.md.

See also: OverviewUserInfo(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits().

userAccountsFile

Filename where simple table with all available user accounts (accountIds) will be saved. Default: accounts.md.

See also: OverviewAccounts(), RequestAccounts().

iListDumpFile

Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: dump.json.

Pandas dataframe with raw instruments data also will be stored to default file dump.xlsx.

See also: DumpInstruments() and DumpInstrumentsAsXLSX().

iList

Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the iListDumpFile.

See also: Listing(), DumpInstruments().

priceModel

PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.

See also: LoadHistory(), ShowHistoryChart() and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator

def SendAPIRequest( self, url: str, reqType: str = 'GET', retry: int = 3, pause: int = 5, debug: bool = False) -> dict:
470    def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5, debug: bool = False) -> dict:
471        """
472        Send GET or POST request to broker server and receive JSON object.
473
474        self.header: must be defining with dictionary of headers.
475        self.body: if define then used as request body. None by default.
476        self.timeout: global request timeout, 15 seconds by default.
477        :param url: url with REST request.
478        :param reqType: send "GET" or "POST" request. "GET" by default.
479        :param retry: how many times retry after first request if an 5xx server errors occurred.
480        :param pause: sleep time in seconds between retries.
481        :param debug: if `True` then print more debug information, e.g. request and response parameters, headers etc.
482        :return: response JSON (dictionary) from broker.
483        """
484        if reqType not in ("GET", "POST"):
485            uLogger.error("You can define request type: 'GET' or 'POST'!")
486            raise Exception("Incorrect value")
487
488        if debug:
489            uLogger.debug("Request parameters:")
490            uLogger.debug("    - REST API URL: {}".format(url))
491            uLogger.debug("    - request type: {}".format(reqType))
492            uLogger.debug("    - headers: {}".format(str(self.headers).replace(self.token, "*** request token ***")))
493            uLogger.debug("    - body: {}".format(self.body))
494
495        # fast hack to avoid all operations with some tickers/FIGI
496        responseJSON = {}
497        oK = True
498        for item in self.exclude:
499            if item in url:
500                if debug:
501                    uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude)))
502
503                oK = False
504                break
505
506        if oK:
507            counter = 0
508            response = None
509            errMsg = ""
510
511            while not response and counter <= retry:
512                if reqType == "GET":
513                    response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout)
514
515                if reqType == "POST":
516                    response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout)
517
518                if debug:
519                    uLogger.debug("Response:")
520                    uLogger.debug("    - status code: {}".format(response.status_code))
521                    uLogger.debug("    - reason: {}".format(response.reason))
522                    uLogger.debug("    - body length: {}".format(len(response.text)))
523                    uLogger.debug("    - headers: {}".format(response.headers))
524
525                # Server returns some headers:
526                # - `x-ratelimit-limit` - shows the settings of the current user limit for this method.
527                # - `x-ratelimit-remaining` - the number of remaining requests of this type per minute.
528                # - `x-ratelimit-reset` - time in seconds before resetting the request counter.
529                # See: https://tinkoff.github.io/investAPI/grpc/#kreya
530                if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0":
531                    rateLimitWait = int(response.headers["x-ratelimit-reset"])
532                    uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait))
533                    sleep(rateLimitWait)
534
535                # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
536                if 400 <= response.status_code < 500:
537                    msg = "status code: [{}], response body: {}".format(response.status_code, response.text)
538                    uLogger.debug("    - not oK, but do not retry for 4xx errors, {}".format(msg))
539                    counter = retry + 1
540
541                if 500 <= response.status_code < 600:
542                    errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text)
543                    uLogger.debug("    - not oK, {}".format(errMsg))
544                    counter += 1
545
546                    if counter <= retry:
547                        uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause))
548                        sleep(pause)
549
550            responseJSON = self._ParseJSON(response.text)
551
552            if errMsg:
553                uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/")
554                uLogger.error("    - not oK, {}".format(errMsg))
555
556        return responseJSON

Send GET or POST request to broker server and receive JSON object.

self.header: must be defining with dictionary of headers. self.body: if define then used as request body. None by default. self.timeout: global request timeout, 15 seconds by default.

Parameters
  • url: url with REST request.
  • reqType: send "GET" or "POST" request. "GET" by default.
  • retry: how many times retry after first request if an 5xx server errors occurred.
  • pause: sleep time in seconds between retries.
  • debug: if True then print more debug information, e.g. request and response parameters, headers etc.
Returns

response JSON (dictionary) from broker.

def Listing(self) -> dict:
589    def Listing(self) -> dict:
590        """
591        Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
592
593        :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
594        """
595        uLogger.debug("Requesting all available instruments for current account. Wait, please...")
596        uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES))
597
598        # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService
599        # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list.
600        iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS]
601
602        poolUpdater = ThreadPool(processes=CPU_USAGES)  # create pool for update instruments in parallel mode
603        listing = poolUpdater.map(self._IWrapper, iParams)  # execute update operations
604        poolUpdater.close()
605
606        # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures.
607        # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method
608        iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing}
609
610        # calculate minimum price increment (step) for all instruments and set up instrument's type:
611        for iType in iList.keys():
612            for ticker in iList[iType]:
613                iList[iType][ticker]["type"] = iType
614
615                if "minPriceIncrement" in iList[iType][ticker].keys():
616                    iList[iType][ticker]["step"] = NanoToFloat(
617                        iList[iType][ticker]["minPriceIncrement"]["units"],
618                        iList[iType][ticker]["minPriceIncrement"]["nano"],
619                    )
620
621                else:
622                    iList[iType][ticker]["step"] = 0  # hack to avoid empty value in some instruments, e.g. futures
623
624        return iList

Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.

Returns

Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.

def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
626    def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
627        """
628        Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
629
630        See also: `DumpInstruments()`, `Listing()`.
631
632        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
633                            otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) .
634        """
635        if self.iListDumpFile is None or not self.iListDumpFile:
636            uLogger.error("Output name of dump file must be defined!")
637            raise Exception("Filename required")
638
639        if not self.iList or forceUpdate:
640            self.iList = self.Listing()
641
642        xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx"
643
644        # Save as XLSX with separated sheets for every type of instruments:
645        with pd.ExcelWriter(
646                path=xlsxDumpFile,
647                date_format=TKS_DATE_FORMAT,
648                datetime_format=TKS_DATE_TIME_FORMAT,
649                mode="w",
650        ) as writer:
651            for iType in TKS_INSTRUMENTS:
652                df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index")  # generate pandas object from self.iList dictionary
653                df = df[sorted(df)]  # sorted by column names
654                df = df.applymap(
655                    lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item,
656                    na_action="ignore",
657                )  # converting numbers from nano-type to float in every cell
658                df.to_excel(
659                    writer,
660                    sheet_name=iType,
661                    encoding="UTF-8",
662                    freeze_panes=(1, 1),
663                )  # saving as XLSX-file with freeze first row and column as headers
664
665        uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))

Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.

See also: DumpInstruments(), Listing().

Parameters
  • forceUpdate: if True then at first updates data with Listing() method, otherwise just saves exist iList as XLSX-file (default: dump.xlsx) .
def DumpInstruments(self, forceUpdate: bool = True) -> str:
667    def DumpInstruments(self, forceUpdate: bool = True) -> str:
668        """
669        Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
670        using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file.
671
672        See also: `DumpInstrumentsAsXLSX()`, `Listing()`.
673
674        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
675                            otherwise just saves exist `iList` as JSON-file (default: `dump.json`).
676        :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file.
677        """
678        if self.iListDumpFile is None or not self.iListDumpFile:
679            uLogger.error("Output name of dump file must be defined!")
680            raise Exception("Filename required")
681
682        if not self.iList or forceUpdate:
683            self.iList = self.Listing()
684
685        jsonDump = json.dumps(self.iList, indent=4, sort_keys=False)  # create JSON object as string
686        with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH:
687            fH.write(jsonDump)
688
689        uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile)))
690
691        return jsonDump

Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server using Listing() method. If iListDumpFile string is not empty then also save information to this file.

See also: DumpInstrumentsAsXLSX(), Listing().

Parameters
  • forceUpdate: if True then at first updates data with Listing() method, otherwise just saves exist iList as JSON-file (default: dump.json).
Returns

serialized JSON formatted str with full data of instruments, also saved to the --output JSON-file.

def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str:
693    def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str:
694        """
695        Show information about one instrument defined by json data and prints it in Markdown format.
696
697        See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`.
698
699        :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self.ticker]`
700        :param show: if `True` then also printing information about instrument and its current price.
701        :return: multilines text in Markdown format with information about one instrument.
702        """
703        splitLine = "|                                                             |                                                        |\n"
704        infoText = ""
705
706        if iJSON is not None and iJSON and isinstance(iJSON, dict):
707            info = [
708                "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]),
709                "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
710                "| Parameters                                                  | Values                                                 |\n",
711                "|-------------------------------------------------------------|--------------------------------------------------------|\n",
712                "| Ticker:                                                     | {:<54} |\n".format(iJSON["ticker"]),
713                "| Full name:                                                  | {:<54} |\n".format(iJSON["name"]),
714            ]
715
716            if "sector" in iJSON.keys() and iJSON["sector"]:
717                info.append("| Sector:                                                     | {:<54} |\n".format(iJSON["sector"]))
718
719            info.append("| Country of instrument:                                      | {:<54} |\n".format("{}{}".format(
720                "({}) ".format(iJSON["countryOfRisk"]) if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] else "",
721                iJSON["countryOfRiskName"] if "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"] else "",
722            )))
723
724            info.extend([
725                splitLine,
726                "| FIGI (Financial Instrument Global Identifier):              | {:<54} |\n".format(iJSON["figi"]),
727                "| Exchange:                                                   | {:<54} |\n".format(iJSON["exchange"]),
728            ])
729
730            if "isin" in iJSON.keys() and iJSON["isin"]:
731                info.append("| ISIN (International Securities Identification Number):      | {:<54} |\n".format(iJSON["isin"]))
732
733            if "classCode" in iJSON.keys():
734                info.append("| Class Code (exchange section where instrument is traded):   | {:<54} |\n".format(iJSON["classCode"]))
735
736            info.extend([
737                splitLine,
738                "| Current broker security trading status:                     | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]),
739                splitLine,
740                "| Buy operations allowed:                                     | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"),
741                "| Sale operations allowed:                                    | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"),
742                "| Short positions allowed:                                    | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"),
743            ])
744
745            if iJSON["figi"]:
746                self.figi = iJSON["figi"]
747                iJSON = iJSON | self.RequestTradingStatus()
748
749                info.extend([
750                    splitLine,
751                    "| Limit orders allowed:                                       | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"),
752                    "| Market orders allowed:                                      | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"),
753                    "| API trade allowed:                                          | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"),
754                ])
755
756            info.append(splitLine)
757
758            if "type" in iJSON.keys() and iJSON["type"]:
759                info.append("| Type of the instrument:                                     | {:<54} |\n".format(iJSON["type"]))
760
761            if "futuresType" in iJSON.keys() and iJSON["futuresType"]:
762                info.append("| Futures type:                                               | {:<54} |\n".format(iJSON["futuresType"]))
763
764            if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]:
765                info.append("| IPO date:                                                   | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", "")))
766
767            if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]:
768                info.append("| Released date:                                              | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", "")))
769
770            if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]:
771                info.append("| Rebalancing frequency:                                      | {:<54} |\n".format(iJSON["rebalancingFreq"]))
772
773            if "focusType" in iJSON.keys() and iJSON["focusType"]:
774                info.append("| Focusing type:                                              | {:<54} |\n".format(iJSON["focusType"]))
775
776            if "assetType" in iJSON.keys() and iJSON["assetType"]:
777                info.append("| Asset type:                                                 | {:<54} |\n".format(iJSON["assetType"]))
778
779            if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]:
780                info.append("| Basic asset:                                                | {:<54} |\n".format(iJSON["basicAsset"]))
781
782            if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]:
783                info.append("| Basic asset size:                                           | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"]))))
784
785            if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]:
786                info.append("| ISO currency name:                                          | {:<54} |\n".format(iJSON["isoCurrencyName"]))
787
788            if "currency" in iJSON.keys():
789                info.append("| Payment currency:                                           | {:<54} |\n".format(iJSON["currency"]))
790
791            if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys():
792                info.append("| Nominal currency:                                           | {:<54} |\n".format(iJSON["nominal"]["currency"]))
793
794            if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]:
795                info.append("| First trade date:                                           | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", "")))
796
797            if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]:
798                info.append("| Last trade date:                                            | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", "")))
799
800            if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]:
801                info.append("| Date of expiration:                                         | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", "")))
802
803            if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]:
804                info.append("| State registration date:                                    | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", "")))
805
806            if "placementDate" in iJSON.keys() and iJSON["placementDate"]:
807                info.append("| Placement date:                                             | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", "")))
808
809            if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]:
810                info.append("| Maturity date:                                              | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", "")))
811
812            if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]:
813                info.append("| Perpetual bond:                                             | Yes                                                    |\n")
814
815            if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]:
816                info.append("| Over-the-counter (OTC) securities:                          | Yes                                                    |\n")
817
818            iExt = None
819            if iJSON["type"] == "Bonds":
820                info.extend([
821                    splitLine,
822                    "| Bond issue (size / plan):                                   | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])),
823                    "| Nominal price (100%):                                       | {:<54} |\n".format("{} {}".format(
824                        "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."),
825                        iJSON["nominal"]["currency"],
826                    )),
827                ])
828
829                if "floatingCouponFlag" in iJSON.keys():
830                    info.append("| Floating coupon:                                            | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No"))
831
832                if "amortizationFlag" in iJSON.keys():
833                    info.append("| Amortization:                                               | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No"))
834
835                info.append(splitLine)
836
837                if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]:
838                    info.append("| Number of coupon payments per year:                         | {:<54} |\n".format(iJSON["couponQuantityPerYear"]))
839
840                iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False)  # extended bonds data
841
842                info.extend([
843                    "| Days last to maturity date:                                 | {:<54} |\n".format(iExt["daysToMaturity"][0]),
844                    "| Coupons yield (average coupon daily yield * 365):           | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])),
845                    "| Current price yield (average daily yield * 365):            | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])),
846                ])
847
848                if "aciValue" in iJSON.keys() and iJSON["aciValue"]:
849                    info.append("| Current accumulated coupon income (ACI):                    | {:<54} |\n".format("{:.2f} {}".format(
850                        NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]),
851                        iJSON["aciValue"]["currency"]
852                    )))
853
854            if "currentPrice" in iJSON.keys():
855                info.append(splitLine)
856
857                currency = iJSON["currency"] if "currency" in iJSON.keys() else ""  # nominal currency for bonds, otherwise currency of instrument
858                aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else ""  # payment currency
859
860                bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0  # previous close price of bond
861                bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0  # last price of bond
862                bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0  # max price of bond
863                bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0  # min price of bond
864                bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0  # delta between last deal price and last close
865
866                curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0
867                curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0
868
869                info.extend([
870                    "| Previous close price of the instrument:                     | {:<54} |\n".format("{}{}".format(
871                        "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A",
872                        "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
873                    )),
874                    "| Last deal price of the instrument:                          | {:<54} |\n".format("{}{}".format(
875                        "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A",
876                        "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
877                    )),
878                    "| Changes between last deal price and last close              | {:<54} |\n".format(
879                        "{:.2f}%{}".format(
880                            iJSON["currentPrice"]["changes"],
881                            " ({}{:.2f} {})".format(
882                                "+" if bondChangesDelta > 0 else "",
883                                bondChangesDelta,
884                                aciCurrency
885                            ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format(
886                                "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "",
887                                iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"],
888                                currency
889                            ),
890                        )
891                    ),
892                    "| Current limit price, min / max:                             | {:<54} |\n".format("{}{} / {}{}{}".format(
893                        "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A",
894                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
895                        "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A",
896                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
897                        " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else ""
898                    )),
899                    "| Actual price, sell / buy:                                   | {:<54} |\n".format("{}{} / {}{}{}".format(
900                        "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A",
901                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
902                        "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A",
903                        "%" if iJSON["type"] == "Bonds" else" {}".format(currency),
904                        " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else ""
905                    )),
906                ])
907
908            if "lot" in iJSON.keys():
909                info.append("| Minimum lot to buy:                                         | {:<54} |\n".format(iJSON["lot"]))
910
911            if "step" in iJSON.keys() and iJSON["step"] != 0:
912                info.append("| Minimum price increment (step):                             | {:<54} |\n".format(iJSON["step"]))
913
914            # Add bond payment calendar:
915            if iJSON["type"] == "Bonds":
916                strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False)   # bond payment calendar
917                info.extend(["\n", strCalendar])
918
919            infoText += "".join(info)
920
921            if show:
922                uLogger.info("{}".format(infoText))
923
924            else:
925                uLogger.debug("{}".format(infoText))
926
927            if self.infoFile is not None:
928                with open(self.infoFile, "w", encoding="UTF-8") as fH:
929                    fH.write(infoText)
930
931                uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile)))
932
933        return infoText

Show information about one instrument defined by json data and prints it in Markdown format.

See also: SearchByTicker(), SearchByFIGI(), RequestBondCoupons(), ExtendBondsData(), ShowBondsCalendar() and RequestTradingStatus().

Parameters
  • iJSON: json data of instrument, example: iJSON = self.iList["Shares"][self.ticker]
  • show: if True then also printing information about instrument and its current price.
Returns

multilines text in Markdown format with information about one instrument.

def SearchByTicker( self, requestPrice: bool = False, show: bool = False, debug: bool = False) -> dict:
 935    def SearchByTicker(self, requestPrice: bool = False, show: bool = False, debug: bool = False) -> dict:
 936        """
 937        Search and return raw broker's information about instrument by its ticker.
 938        `ticker` must be defined! If debug=True then print all debug messages.
 939
 940        :param requestPrice: if `False` then do not request current price of instrument (because this is long operation).
 941        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 942        :param debug: if `True` then print all debug console messages.
 943        :return: JSON formatted data with information about instrument.
 944        """
 945        tickerJSON = {}
 946        if debug:
 947            uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self.ticker))
 948
 949        if not self.ticker:
 950            uLogger.warning("self.ticker variable is not be empty!")
 951
 952        else:
 953            if self.ticker in TKS_TICKERS_OR_FIGI_EXCLUDED:
 954                uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self.ticker))
 955                raise Exception("Instrument not allowed")
 956
 957            if not self.iList:
 958                self.iList = self.Listing()
 959
 960            if self.ticker in self.iList["Shares"].keys():
 961                tickerJSON = self.iList["Shares"][self.ticker]
 962                if debug:
 963                    uLogger.debug("Ticker [{}] found in shares list".format(self.ticker))
 964
 965            elif self.ticker in self.iList["Currencies"].keys():
 966                tickerJSON = self.iList["Currencies"][self.ticker]
 967                if debug:
 968                    uLogger.debug("Ticker [{}] found in currencies list".format(self.ticker))
 969
 970            elif self.ticker in self.iList["Bonds"].keys():
 971                tickerJSON = self.iList["Bonds"][self.ticker]
 972                if debug:
 973                    uLogger.debug("Ticker [{}] found in bonds list".format(self.ticker))
 974
 975            elif self.ticker in self.iList["Etfs"].keys():
 976                tickerJSON = self.iList["Etfs"][self.ticker]
 977                if debug:
 978                    uLogger.debug("Ticker [{}] found in etfs list".format(self.ticker))
 979
 980            elif self.ticker in self.iList["Futures"].keys():
 981                tickerJSON = self.iList["Futures"][self.ticker]
 982                if debug:
 983                    uLogger.debug("Ticker [{}] found in futures list".format(self.ticker))
 984
 985        if tickerJSON:
 986            self.figi = tickerJSON["figi"]
 987
 988            if requestPrice:
 989                tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False)
 990
 991                if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None:
 992                    tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"]
 993
 994                else:
 995                    tickerJSON["currentPrice"]["changes"] = 0
 996
 997            if show:
 998                self.ShowInstrumentInfo(iJSON=tickerJSON, show=True)  # print info as Markdown text
 999
1000        else:
1001            if show:
1002                uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self.ticker))
1003
1004        return tickerJSON

Search and return raw broker's information about instrument by its ticker. ticker must be defined! If debug=True then print all debug messages.

Parameters
  • requestPrice: if False then do not request current price of instrument (because this is long operation).
  • show: if False then do not run ShowInstrumentInfo() method and do not print info to the console.
  • debug: if True then print all debug console messages.
Returns

JSON formatted data with information about instrument.

def SearchByFIGI( self, requestPrice: bool = False, show: bool = False, debug: bool = False) -> dict:
1006    def SearchByFIGI(self, requestPrice: bool = False, show: bool = False, debug: bool = False) -> dict:
1007        """
1008        Search and return raw broker's information about instrument by its FIGI.
1009        `figi` must be defined! If debug=True then print all debug messages.
1010
1011        :param requestPrice: if `False` then do not request current price of instrument (it's long operation).
1012        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
1013        :param debug: if `True` then print all debug console messages.
1014        :return: JSON formatted data with information about instrument.
1015        """
1016        figiJSON = {}
1017        if debug:
1018            uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self.figi))
1019
1020        if not self.figi:
1021            uLogger.warning("self.figi variable is not be empty!")
1022
1023        else:
1024            if self.figi in TKS_TICKERS_OR_FIGI_EXCLUDED:
1025                uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self.figi))
1026                raise Exception("Instrument not allowed")
1027
1028            if not self.iList:
1029                self.iList = self.Listing()
1030
1031            for item in self.iList["Shares"].keys():
1032                if self.figi == self.iList["Shares"][item]["figi"]:
1033                    figiJSON = self.iList["Shares"][item]
1034
1035                    if debug:
1036                        uLogger.debug("FIGI [{}] found in shares list".format(self.figi))
1037
1038                    break
1039
1040            if not figiJSON:
1041                for item in self.iList["Currencies"].keys():
1042                    if self.figi == self.iList["Currencies"][item]["figi"]:
1043                        figiJSON = self.iList["Currencies"][item]
1044
1045                        if debug:
1046                            uLogger.debug("FIGI [{}] found in currencies list".format(self.figi))
1047
1048                        break
1049
1050            if not figiJSON:
1051                for item in self.iList["Bonds"].keys():
1052                    if self.figi == self.iList["Bonds"][item]["figi"]:
1053                        figiJSON = self.iList["Bonds"][item]
1054
1055                        if debug:
1056                            uLogger.debug("FIGI [{}] found in bonds list".format(self.figi))
1057
1058                        break
1059
1060            if not figiJSON:
1061                for item in self.iList["Etfs"].keys():
1062                    if self.figi == self.iList["Etfs"][item]["figi"]:
1063                        figiJSON = self.iList["Etfs"][item]
1064
1065                        if debug:
1066                            uLogger.debug("FIGI [{}] found in etfs list".format(self.figi))
1067
1068                        break
1069
1070            if not figiJSON:
1071                for item in self.iList["Futures"].keys():
1072                    if self.figi == self.iList["Futures"][item]["figi"]:
1073                        figiJSON = self.iList["Futures"][item]
1074
1075                        if debug:
1076                            uLogger.debug("FIGI [{}] found in futures list".format(self.figi))
1077
1078                        break
1079
1080        if figiJSON:
1081            self.figi = figiJSON["figi"]
1082            self.ticker = figiJSON["ticker"]
1083
1084            if requestPrice:
1085                figiJSON["currentPrice"] = self.GetCurrentPrices(show=False)
1086
1087                if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None:
1088                    figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"]
1089
1090                else:
1091                    figiJSON["currentPrice"]["changes"] = 0
1092
1093            if show:
1094                self.ShowInstrumentInfo(iJSON=figiJSON, show=True)  # print info as Markdown text
1095
1096        else:
1097            if show:
1098                uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self.figi))
1099
1100        return figiJSON

Search and return raw broker's information about instrument by its FIGI. figi must be defined! If debug=True then print all debug messages.

Parameters
  • requestPrice: if False then do not request current price of instrument (it's long operation).
  • show: if False then do not run ShowInstrumentInfo() method and do not print info to the console.
  • debug: if True then print all debug console messages.
Returns

JSON formatted data with information about instrument.

def GetCurrentPrices(self, show: bool = True) -> dict:
1102    def GetCurrentPrices(self, show: bool = True) -> dict:
1103        """
1104        Get and show Depth of Market with current prices of the instrument. If an error occurred then returns an empty record:
1105        `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`.
1106
1107        See also:
1108
1109        :param show: if `True` then print DOM to log and console.
1110        :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`.
1111        """
1112        prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0}
1113
1114        if self.depth < 1:
1115            uLogger.error("Depth of Market (DOM) must be >=1!")
1116            raise Exception("Incorrect value")
1117
1118        if not (self.ticker or self.figi):
1119            uLogger.error("self.ticker or self.figi variables must be defined!")
1120            raise Exception("Ticker or FIGI required")
1121
1122        if self.ticker and not self.figi:
1123            instrumentByTicker = self.SearchByTicker(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1124            self.figi = instrumentByTicker["figi"] if instrumentByTicker else ""
1125
1126        if not self.ticker and self.figi:
1127            instrumentByFigi = self.SearchByFIGI(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1128            self.ticker = instrumentByFigi["ticker"] if instrumentByFigi else ""
1129
1130        if not self.figi:
1131            uLogger.error("FIGI is not defined!")
1132            raise Exception("Ticker or FIGI required")
1133
1134        else:
1135            uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self.ticker, self.figi))
1136
1137            # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1138            priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook"
1139            self.body = str({"figi": self.figi, "depth": self.depth})
1140            pricesResponse = self.SendAPIRequest(priceURL, reqType="POST")
1141
1142            if pricesResponse:
1143                # list of dicts with sellers orders:
1144                prices["buy"] = [{"price": NanoToFloat(item["price"]["units"], item["price"]["nano"]), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]]
1145
1146                # list of dicts with buyers orders:
1147                prices["sell"] = [{"price": NanoToFloat(item["price"]["units"], item["price"]["nano"]), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]]
1148
1149                # max price of instrument at this time:
1150                prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None
1151
1152                # min price of instrument at this time:
1153                prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None
1154
1155                # last price of deal with instrument:
1156                prices["lastPrice"] = NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]) if "lastPrice" in pricesResponse.keys() else 0
1157
1158                # last close price of instrument:
1159                prices["closePrice"] = NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]) if "closePrice" in pricesResponse.keys() else 0
1160
1161            else:
1162                uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi))
1163                uLogger.debug("Server response: {}".format(pricesResponse))
1164
1165            if show:
1166                if prices["buy"] or prices["sell"]:
1167                    info = [
1168                        "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format(
1169                            datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
1170                            self.ticker,
1171                            self.figi,
1172                            self.depth,
1173                        ),
1174                        uLog.sepShort, "\n",
1175                        " Orders of Buyers   | Orders of Sellers\n",
1176                        uLog.sepShort, "\n",
1177                        " Sell prices (vol.) | Buy prices (vol.)\n",
1178                        uLog.sepShort, "\n",
1179                    ]
1180
1181                    if not prices["buy"]:
1182                        info.append("                    | No orders!\n")
1183                        sumBuy = 0
1184
1185                    else:
1186                        sumBuy = sum([x["quantity"] for x in prices["buy"]])
1187                        maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True)
1188                        for item in maxMinSorted:
1189                            info.append("                    | {} ({})\n".format(item["price"], item["quantity"]))
1190
1191                    if not prices["sell"]:
1192                        info.append("No orders!          |\n")
1193                        sumSell = 0
1194
1195                    else:
1196                        sumSell = sum([x["quantity"] for x in prices["sell"]])
1197                        for item in prices["sell"]:
1198                            info.append("{:>19} |\n".format("{} ({})".format(item["price"], item["quantity"])))
1199
1200                    info.extend([
1201                        uLog.sepShort, "\n",
1202                        "{:>19} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)),
1203                        uLog.sepShort, "\n",
1204                    ])
1205
1206                    infoText = "".join(info)
1207
1208                    uLogger.info("Current prices in order book:\n\n{}".format(infoText))
1209
1210                else:
1211                    uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi))
1212
1213        return prices

Get and show Depth of Market with current prices of the instrument. If an error occurred then returns an empty record: {"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}.

See also:

Parameters
  • show: if True then print DOM to log and console.
Returns

orders book dict with lists of current buy and sell prices: {"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}.

def ShowInstrumentsInfo(self, show: bool = True) -> str:
1215    def ShowInstrumentsInfo(self, show: bool = True) -> str:
1216        """
1217        This method get and show information about all available broker instruments for current user account.
1218        If `instrumentsFile` string is not empty then also save information to this file.
1219
1220        :param show: if `True` then print results to console, if `False` - print only to file.
1221        :return: multi-lines string with all available broker instruments
1222        """
1223        if not self.iList:
1224            self.iList = self.Listing()
1225
1226        info = [
1227            "# All available instruments from Tinkoff Broker server for current user token\n\n",
1228            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1229        ]
1230
1231        # add instruments count by type:
1232        for iType in self.iList.keys():
1233            info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType])))
1234
1235        headerLine = "| Ticker       | Full name                                                 | FIGI         | Cur | Lot     | Step       |\n"
1236        splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n"
1237
1238        # generating info tables with all instruments by type:
1239        for iType in self.iList.keys():
1240            info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine])
1241
1242            for instrument in self.iList[iType].keys():
1243                iName = self.iList[iType][instrument]["name"]  # instrument's name
1244                if len(iName) > 57:
1245                    iName = "{}...".format(iName[:54])  # right trim for a long string
1246
1247                info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format(
1248                    self.iList[iType][instrument]["ticker"],
1249                    iName,
1250                    self.iList[iType][instrument]["figi"],
1251                    self.iList[iType][instrument]["currency"],
1252                    self.iList[iType][instrument]["lot"],
1253                    "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0,
1254                ))
1255
1256        infoText = "".join(info)
1257
1258        if show:
1259            uLogger.info(infoText)
1260
1261        if self.instrumentsFile:
1262            with open(self.instrumentsFile, "w", encoding="UTF-8") as fH:
1263                fH.write(infoText)
1264
1265            uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile)))
1266
1267        return infoText

This method get and show information about all available broker instruments for current user account. If instrumentsFile string is not empty then also save information to this file.

Parameters
  • show: if True then print results to console, if False - print only to file.
Returns

multi-lines string with all available broker instruments

def SearchInstruments(self, pattern: str, show: bool = True) -> dict:
1269    def SearchInstruments(self, pattern: str, show: bool = True) -> dict:
1270        """
1271        This method search and show information about instruments by part of its ticker, FIGI or name.
1272        If `searchResultsFile` string is not empty then also save information to this file.
1273
1274        :param pattern: string with part of ticker, FIGI or instrument's name.
1275        :param show: if `True` then print results to console, if `False` - return list of result only.
1276        :return: list of dictionaries with all found instruments.
1277        """
1278        if not self.iList:
1279            self.iList = self.Listing()
1280
1281        searchResults = {iType: {} for iType in self.iList}  # same as iList but will contains only filtered instruments
1282        compiledPattern = re.compile(pattern, re.IGNORECASE)
1283
1284        for iType in self.iList:
1285            for instrument in self.iList[iType].values():
1286                searchResult = compiledPattern.search(" ".join(
1287                    [instrument["ticker"], instrument["figi"], instrument["name"]]
1288                ))
1289
1290                if searchResult:
1291                    searchResults[iType][instrument["ticker"]] = instrument
1292
1293        resultsLen = sum([len(searchResults[iType]) for iType in searchResults])
1294        info = [
1295            "# Search results\n\n",
1296            "* **Search pattern:** [{}]\n".format(pattern),
1297            "* **Found instruments:** [{}]\n\n".format(resultsLen),
1298            "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n"
1299        ]
1300        infoShort = info[:]
1301
1302        headerLine = "| Type       | Ticker       | Full name                                                      | FIGI         |\n"
1303        splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n"
1304        skippedLine = "| ...        | ...          | ...                                                            | ...          |\n"
1305
1306        if resultsLen == 0:
1307            info.append("\nNo results\n")
1308            infoShort.append("\nNo results\n")
1309            uLogger.warning("No results. Try changing your search pattern.")
1310
1311        else:
1312            for iType in searchResults:
1313                iTypeValuesCount = len(searchResults[iType].values())
1314                if iTypeValuesCount > 0:
1315                    info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1316                    infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1317
1318                    for instrument in searchResults[iType].values():
1319                        info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format(
1320                            instrument["type"],
1321                            instrument["ticker"],
1322                            "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"],  # right trim for a long string
1323                            instrument["figi"],
1324                        ))
1325
1326                    if iTypeValuesCount <= 5:
1327                        infoShort.extend(info[-iTypeValuesCount:])
1328
1329                    else:
1330                        infoShort.extend(info[-5:])
1331                        infoShort.append(skippedLine)
1332
1333        infoText = "".join(info)
1334        infoTextShort = "".join(infoShort)
1335
1336        if show:
1337            uLogger.info(infoTextShort)
1338            uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`")
1339
1340        if self.searchResultsFile:
1341            with open(self.searchResultsFile, "w", encoding="UTF-8") as fH:
1342                fH.write(infoText)
1343
1344            uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile)))
1345
1346        return searchResults

This method search and show information about instruments by part of its ticker, FIGI or name. If searchResultsFile string is not empty then also save information to this file.

Parameters
  • pattern: string with part of ticker, FIGI or instrument's name.
  • show: if True then print results to console, if False - return list of result only.
Returns

list of dictionaries with all found instruments.

def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1348    def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1349        """
1350        Creating list with unique instrument FIGIs from input list of tickers or FIGIs.
1351
1352        :param instruments: list of strings with tickers or FIGIs.
1353        :return: list with unique instrument FIGIs only.
1354        """
1355        requestedInstruments = []
1356        for iName in instruments:
1357            if iName not in self.aliases.keys():
1358                if iName not in requestedInstruments:
1359                    requestedInstruments.append(iName)
1360
1361            else:
1362                if iName not in requestedInstruments:
1363                    if self.aliases[iName] not in requestedInstruments:
1364                        requestedInstruments.append(self.aliases[iName])
1365
1366        uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments))
1367
1368        onlyUniqueFIGIs = []
1369        for iName in requestedInstruments:
1370            if iName in TKS_TICKERS_OR_FIGI_EXCLUDED:
1371                continue
1372
1373            self.ticker = iName
1374            iData = self.SearchByTicker(requestPrice=False)  # trying to find instrument by ticker
1375
1376            if not iData:
1377                self.ticker = ""
1378                self.figi = iName
1379
1380                iData = self.SearchByFIGI(requestPrice=False)  # trying to find instrument by FIGI
1381
1382                if not iData:
1383                    self.figi = ""
1384                    uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName))
1385
1386            if iData and iData["figi"] not in onlyUniqueFIGIs:
1387                onlyUniqueFIGIs.append(iData["figi"])
1388
1389        uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs))
1390
1391        return onlyUniqueFIGIs

Creating list with unique instrument FIGIs from input list of tickers or FIGIs.

Parameters
  • instruments: list of strings with tickers or FIGIs.
Returns

list with unique instrument FIGIs only.

def GetListOfPrices(self, instruments: list, show: bool = False) -> list:
1393    def GetListOfPrices(self, instruments: list, show: bool = False) -> list:
1394        """
1395        This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
1396        See limits: https://tinkoff.github.io/investAPI/limits/
1397        If `pricesFile` string is not empty then also save information to this file.
1398
1399        :param instruments: list of strings with tickers or FIGIs.
1400        :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`.
1401        :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1402                 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods.
1403        """
1404        if instruments is None or not instruments:
1405            uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!")
1406            raise Exception("Ticker or FIGI required")
1407
1408        onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments)
1409
1410        uLogger.debug("Requesting current prices from Tinkoff Broker server...")
1411
1412        iList = []  # trying to get info and current prices about all unique instruments:
1413        for self.figi in onlyUniqueFIGIs:
1414            iData = self.SearchByFIGI(requestPrice=True)
1415            iList.append(iData)
1416
1417        self.ShowListOfPrices(iList, show)
1418
1419        return iList

This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation! See limits: https://tinkoff.github.io/investAPI/limits/ If pricesFile string is not empty then also save information to this file.

Parameters
  • instruments: list of strings with tickers or FIGIs.
  • show: if True then prints prices to console, if False - prints only to file pricesFile.
Returns

list of instruments looks like [{some ticker info, "currentPrice": {current prices}}, {...}, ...]. One item is dict returned by SearchByTicker() or SearchByFIGI() methods.

def ShowListOfPrices(self, iList: list, show: bool = True) -> str:
1421    def ShowListOfPrices(self, iList: list, show: bool = True) -> str:
1422        """
1423        Show table contains current prices of given instruments.
1424
1425        :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1426                      One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods.
1427        :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`.
1428        :return: multilines text in Markdown format as a table contains current prices.
1429        """
1430        infoText = ""
1431
1432        if show or self.pricesFile:
1433            info = [
1434                "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1435                "| Ticker       | FIGI         | Type       | Prev. close | Last price  | Chg. %   | Day limits min/max  | Actual sell / buy   | Curr. |\n",
1436                "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n",
1437            ]
1438
1439            for item in iList:
1440                info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format(
1441                    item["ticker"],
1442                    item["figi"],
1443                    item["type"],
1444                    "{:.2f}".format(float(item["currentPrice"]["closePrice"])),
1445                    "{:.2f}".format(float(item["currentPrice"]["lastPrice"])),
1446                    "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])),
1447                    "{} / {}".format(
1448                        item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A",
1449                        item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A",
1450                    ),
1451                    "{} / {}".format(
1452                        item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A",
1453                        item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A",
1454                    ),
1455                    item["currency"],
1456                ))
1457
1458            infoText = "".join(info)
1459
1460            if show:
1461                uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText))
1462
1463            if self.pricesFile:
1464                with open(self.pricesFile, "w", encoding="UTF-8") as fH:
1465                    fH.write(infoText)
1466
1467                uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile)))
1468
1469        return infoText

Show table contains current prices of given instruments.

Parameters
  • **iList: list of instruments looks like [{some ticker info, "currentPrice"**: {current prices}}, {...}, ...]. One item is dict returned by SearchByTicker(requestPrice=True) or by SearchByFIGI(requestPrice=True) methods.
  • show: if True then prints prices to console, if False - prints only to file pricesFile.
Returns

multilines text in Markdown format as a table contains current prices.

def RequestTradingStatus(self) -> dict:
1471    def RequestTradingStatus(self) -> dict:
1472        """
1473        Requesting trading status for the instrument defined by `figi` variable.
1474        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus
1475        Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
1476
1477        :return: dictionary with trading status attributes. Response example:
1478                 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING",
1479                  "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}`
1480        """
1481        if self.figi is None or not self.figi:
1482            uLogger.error("Variable `figi` must be defined for using this method!")
1483            raise Exception("FIGI required")
1484
1485        uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self.figi))
1486
1487        self.body = str({"figi": self.figi, "instrumentId": self.figi})
1488        tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus"
1489        tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST")
1490
1491        uLogger.debug("Records about current trading status successfully received")
1492
1493        return tradingStatus

Requesting trading status for the instrument defined by figi variable. REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest

Returns

dictionary with trading status attributes. Response example: {"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}

def RequestPortfolio(self) -> dict:
1495    def RequestPortfolio(self) -> dict:
1496        """
1497        Requesting actual user's portfolio for current `accountId`.
1498        REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
1499        Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
1500
1501        :return: dictionary with user's portfolio.
1502        """
1503        if self.accountId is None or not self.accountId:
1504            uLogger.error("Variable `accountId` must be defined for using this method!")
1505            raise Exception("Account ID required")
1506
1507        uLogger.debug("Requesting current actual user's portfolio. Wait, please...")
1508
1509        self.body = str({"accountId": self.accountId})
1510        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio"
1511        rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST")
1512
1513        uLogger.debug("Records about user's portfolio successfully received")
1514
1515        return rawPortfolio

Requesting actual user's portfolio for current accountId. REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest

Returns

dictionary with user's portfolio.

def RequestPositions(self) -> dict:
1517    def RequestPositions(self) -> dict:
1518        """
1519        Requesting open positions by currencies and instruments for current `accountId`.
1520        REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
1521        Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
1522
1523        :return: dictionary with open positions by instruments.
1524        """
1525        if self.accountId is None or not self.accountId:
1526            uLogger.error("Variable `accountId` must be defined for using this method!")
1527            raise Exception("Account ID required")
1528
1529        uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...")
1530
1531        self.body = str({"accountId": self.accountId})
1532        positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions"
1533        rawPositions = self.SendAPIRequest(positionsURL, reqType="POST")
1534
1535        uLogger.debug("Records about current open positions successfully received")
1536
1537        return rawPositions

Requesting open positions by currencies and instruments for current accountId. REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest

Returns

dictionary with open positions by instruments.

def RequestPendingOrders(self) -> list:
1539    def RequestPendingOrders(self) -> list:
1540        """
1541        Requesting current actual pending orders for current `accountId`.
1542        REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
1543        Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
1544
1545        :return: list of dictionaries with pending orders.
1546        """
1547        if self.accountId is None or not self.accountId:
1548            uLogger.error("Variable `accountId` must be defined for using this method!")
1549            raise Exception("Account ID required")
1550
1551        uLogger.debug("Requesting current actual pending orders. Wait, please...")
1552
1553        self.body = str({"accountId": self.accountId})
1554        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders"
1555        rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"]
1556
1557        uLogger.debug("[{}] records about pending orders received".format(len(rawOrders)))
1558
1559        return rawOrders

Requesting current actual pending orders for current accountId. REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest

Returns

list of dictionaries with pending orders.

def RequestStopOrders(self) -> list:
1561    def RequestStopOrders(self) -> list:
1562        """
1563        Requesting current actual stop orders for current `accountId`.
1564        REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
1565        Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
1566
1567        :return: list of dictionaries with stop orders.
1568        """
1569        if self.accountId is None or not self.accountId:
1570            uLogger.error("Variable `accountId` must be defined for using this method!")
1571            raise Exception("Account ID required")
1572
1573        uLogger.debug("Requesting current actual stop orders. Wait, please...")
1574
1575        self.body = str({"accountId": self.accountId})
1576        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders"
1577        rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"]
1578
1579        uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders)))
1580
1581        return rawStopOrders

Requesting current actual stop orders for current accountId. REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest

Returns

list of dictionaries with stop orders.

def Overview(self, show: bool = False, details: str = 'full') -> dict:
1583    def Overview(self, show: bool = False, details: str = "full") -> dict:
1584        """
1585        Get portfolio: all open positions, orders and some statistics for current `accountId`.
1586        If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile`
1587        are defined then also save information to file.
1588
1589        WARNING! It is not recommended to run this method too many times in a loop! The server receives
1590        many requests about the state of the portfolio, and then, based on the received data, a large number
1591        of calculation and statistics are collected.
1592
1593        :param show: if `False` then only dictionary returns, if `True` then show more debug information.
1594        :param details: how detailed should the information be? You should specify one of strings:
1595                        `full` - shows full available information about portfolio status (by default),
1596                        `positions` - shows only open positions,
1597                        `digest` - show a short digest of the portfolio status,
1598                        `analytics` - shows only the analytics section and the distribution of the portfolio by various categories,
1599                        `orders` - shows only sections of open limits and stop orders.
1600        :return: dictionary with client's raw portfolio and some statistics.
1601        """
1602        if self.accountId is None or not self.accountId:
1603            uLogger.error("Variable `accountId` must be defined for using this method!")
1604            raise Exception("Account ID required")
1605
1606        view = {
1607            "raw": {  # --- raw portfolio responses from broker with user portfolio data:
1608                "headers": {},  # list of dictionaries, response headers without "positions" section
1609                "Currencies": [],  # list of dictionaries, open trades with currencies from "positions" section
1610                "Shares": [],  # list of dictionaries, open trades with shares from "positions" section
1611                "Bonds": [],  # list of dictionaries, open trades with bonds from "positions" section
1612                "Etfs": [],  # list of dictionaries, open trades with etfs from "positions" section
1613                "Futures": [],  # list of dictionaries, open trades with futures from "positions" section
1614                "positions": {},  # raw response from broker: dictionary with current available or blocked currencies and instruments for client
1615                "orders": [],  # raw response from broker: list of dictionaries with all pending (market) orders
1616                "stopOrders": [],  # raw response from broker: list of dictionaries with all stop orders
1617                "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}},  # dict with prices of all currencies in RUB
1618            },
1619            "stat": {  # --- some statistics calculated using "raw" sections:
1620                "portfolioCostRUB": 0.,  # portfolio cost in RUB (Russian Rouble)
1621                "availableRUB": 0.,  # available rubles (without other currencies)
1622                "blockedRUB": 0.,  # blocked sum in Russian Rouble
1623                "totalChangesRUB": 0.,  # changes for all open trades in RUB
1624                "totalChangesPercentRUB": 0.,  # changes for all open trades in percents
1625                "allCurrenciesCostRUB": 0.,  # costs of all currencies (include rubles) in RUB
1626                "sharesCostRUB": 0.,  # costs of all shares in RUB
1627                "bondsCostRUB": 0.,  # costs of all bonds in RUB
1628                "etfsCostRUB": 0.,  # costs of all etfs in RUB
1629                "futuresCostRUB": 0.,  # costs of all futures in RUB
1630                "Currencies": [],  # list of dictionaries of all currencies statistics
1631                "Shares": [],  # list of dictionaries of all shares statistics
1632                "Bonds": [],  # list of dictionaries of all bonds statistics
1633                "Etfs": [],  # list of dictionaries of all etfs statistics
1634                "Futures": [],  # list of dictionaries of all futures statistics
1635                "orders": [],  # list of dictionaries of all pending (market) orders and it's parameters
1636                "stopOrders": [],  # list of dictionaries of all stop orders and it's parameters
1637                "blockedCurrencies": {},  # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21}
1638                "blockedInstruments": {},  # dict with blocked  by FIGI, e.g. {}
1639                "funds": {},  # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1640            },
1641            "analytics": {  # --- some analytics of portfolio:
1642                "distrByAssets": {},  # portfolio distribution by assets
1643                "distrByCompanies": {},  # portfolio distribution by companies
1644                "distrBySectors": {},  # portfolio distribution by sectors
1645                "distrByCurrencies": {},  # portfolio distribution by currencies
1646                "distrByCountries": {},  # portfolio distribution by countries
1647            }
1648        }
1649
1650        details = details.lower()
1651        availableDetails = ["full", "positions", "digest", "analytics", "orders"]
1652        if details not in availableDetails:
1653            details = "full"
1654            uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails))
1655
1656        uLogger.debug("Requesting portfolio of a client. Wait, please...")
1657
1658        portfolioResponse = self.RequestPortfolio()  # current user's portfolio (dict)
1659        view["raw"]["positions"] = self.RequestPositions()  # current open positions by instruments (dict)
1660        view["raw"]["orders"] = self.RequestPendingOrders()  # current actual pending orders (list)
1661        view["raw"]["stopOrders"] = self.RequestStopOrders()  # current actual stop orders (list)
1662
1663        # save response headers without "positions" section:
1664        for key in portfolioResponse.keys():
1665            if key != "positions":
1666                view["raw"]["headers"][key] = portfolioResponse[key]
1667
1668            else:
1669                continue
1670
1671        # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation
1672        # Type of instrument must be only one of supported types in TKS_INSTRUMENTS
1673        for item in portfolioResponse["positions"]:
1674            if item["instrumentType"] == "currency":
1675                self.figi = item["figi"]
1676                curr = self.SearchByFIGI(requestPrice=False)
1677
1678                # current price of currency in RUB:
1679                view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = {
1680                    "name": curr["name"],
1681                    "currentPrice": NanoToFloat(
1682                        item["currentPrice"]["units"],
1683                        item["currentPrice"]["nano"]
1684                    ),
1685                }
1686
1687                view["raw"]["Currencies"].append(item)
1688
1689            elif item["instrumentType"] == "share":
1690                view["raw"]["Shares"].append(item)
1691
1692            elif item["instrumentType"] == "bond":
1693                view["raw"]["Bonds"].append(item)
1694
1695            elif item["instrumentType"] == "etf":
1696                view["raw"]["Etfs"].append(item)
1697
1698            elif item["instrumentType"] == "futures":
1699                view["raw"]["Futures"].append(item)
1700
1701            else:
1702                continue
1703
1704        # how many volume of currencies (by ISO currency name) are blocked:
1705        for item in view["raw"]["positions"]["blocked"]:
1706            blocked = NanoToFloat(item["units"], item["nano"])
1707            if blocked > 0:
1708                view["stat"]["blockedCurrencies"][item["currency"]] = blocked
1709
1710        # how many volume of instruments (by FIGI) are blocked:
1711        for item in view["raw"]["positions"]["securities"]:
1712            blocked = int(item["blocked"])
1713            if blocked > 0:
1714                view["stat"]["blockedInstruments"][item["figi"]] = blocked
1715
1716        allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]}
1717
1718        if "rub" in allBlocked.keys():
1719            view["stat"]["blockedRUB"] = allBlocked["rub"]  # blocked rubles
1720
1721        # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies:
1722        view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"])
1723        view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"])
1724        view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"])
1725        view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"])
1726        view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"])
1727        view["stat"]["portfolioCostRUB"] = sum([
1728            view["stat"]["allCurrenciesCostRUB"],
1729            view["stat"]["sharesCostRUB"],
1730            view["stat"]["bondsCostRUB"],
1731            view["stat"]["etfsCostRUB"],
1732            view["stat"]["futuresCostRUB"],
1733        ])
1734
1735        # --- calculating some portfolio statistics:
1736        byComp = {}  # distribution by companies
1737        bySect = {}  # distribution by sectors
1738        byCurr = {}  # distribution by currencies (include RUB)
1739        unknownCountryName = "All other countries"  # default name for instruments without "countryOfRisk" and "countryOfRiskName"
1740        byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}}  # distribution by countries (currencies are included in their countries)
1741
1742        for item in portfolioResponse["positions"]:
1743            self.figi = item["figi"]
1744            instrument = self.SearchByFIGI(requestPrice=False)  # full raw info about instrument by FIGI
1745
1746            if instrument:
1747                if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys():
1748                    blocked = allBlocked[instrument["nominal"]["currency"]]  # blocked volume of currency
1749
1750                elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys():
1751                    blocked = allBlocked[item["figi"]]  # blocked volume of other instruments
1752
1753                else:
1754                    blocked = 0
1755
1756                volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"])  # available volume of instrument
1757                lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"])  # available volume in lots of instrument
1758                direction = "Long" if lots >= 0 else "Short"  # direction of an instrument's position: short or long
1759                curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"])  # current instrument's price
1760                average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"])  # current average position price
1761                profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"])  # expected profit at current moment
1762                currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"]  # currency name rub, usd, eur etc.
1763                cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume  # current cost of all volume of instrument in basic asset
1764                baseCurrencyName = item["currentPrice"]["currency"]  # name of base currency (rub)
1765                countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName
1766                costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"]  # cost in rubles
1767                percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.  # instrument's part in percent of full portfolio cost
1768
1769                statData = {
1770                    "figi": item["figi"],  # FIGI from REST API "GetPortfolio" method
1771                    "ticker": instrument["ticker"],  # ticker by FIGI
1772                    "currency": currency,  # currency name rub, usd, eur etc. for instrument price
1773                    "volume": volume,  # available volume of instrument
1774                    "lots": lots,  # volume in lots of instrument
1775                    "direction": direction,  # direction of an instrument's position: short or long
1776                    "blocked": blocked,  # blocked volume of currency or instrument
1777                    "currentPrice": curPrice,  # current instrument's price in basic asset
1778                    "average": average,  # current average position price
1779                    "cost": cost,  # current cost of all volume of instrument in basic asset
1780                    "baseCurrencyName": baseCurrencyName,  # name of base currency (rub)
1781                    "costRUB": costRUB,  # cost of instrument in ruble
1782                    "percentCostRUB": percentCostRUB,  # instrument's part in percent of full portfolio cost in RUB
1783                    "profit": profit,  # expected profit at current moment
1784                    "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0,  # expected percents of profit at current moment for this instrument
1785                    "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other",
1786                    "name": instrument["name"] if "name" in instrument.keys() else "",  # human-readable names of instruments
1787                    "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "",  # ISO name for currencies only
1788                    "country": countryName,  # e.g. "[RU] Российская Федерация" or unknownCountryName
1789                    "step": instrument["step"],  # minimum price increment
1790                }
1791
1792                # adding distribution by unique countries:
1793                if statData["country"] not in byCountry.keys():
1794                    byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB}
1795
1796                else:
1797                    byCountry[statData["country"]]["cost"] += costRUB
1798                    byCountry[statData["country"]]["percent"] += percentCostRUB
1799
1800                if item["instrumentType"] != "currency":
1801                    # adding distribution by unique companies:
1802                    if statData["name"]:
1803                        if statData["name"] not in byComp.keys():
1804                            byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB}
1805
1806                        else:
1807                            byComp[statData["name"]]["cost"] += costRUB
1808                            byComp[statData["name"]]["percent"] += percentCostRUB
1809
1810                    # adding distribution by unique sectors:
1811                    if statData["sector"] not in bySect.keys():
1812                        bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB}
1813
1814                    else:
1815                        bySect[statData["sector"]]["cost"] += costRUB
1816                        bySect[statData["sector"]]["percent"] += percentCostRUB
1817
1818                # adding distribution by unique currencies:
1819                if currency not in byCurr.keys():
1820                    byCurr[currency] = {
1821                        "name": view["raw"]["currenciesCurrentPrices"][currency]["name"],
1822                        "cost": costRUB,
1823                        "percent": percentCostRUB
1824                    }
1825
1826                else:
1827                    byCurr[currency]["cost"] += costRUB
1828                    byCurr[currency]["percent"] += percentCostRUB
1829
1830                # saving statistics for every instrument:
1831                if item["instrumentType"] == "currency":
1832                    view["stat"]["Currencies"].append(statData)
1833
1834                    # update dict with free funds for trading (total - blocked) by currencies
1835                    # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1836                    view["stat"]["funds"][currency] = {
1837                        "total": volume,
1838                        "totalCostRUB": costRUB,  # total volume cost in rubles
1839                        "free": volume - blocked,
1840                        "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0,  # free volume cost in rubles
1841                    }
1842
1843                elif item["instrumentType"] == "share":
1844                    view["stat"]["Shares"].append(statData)
1845
1846                elif item["instrumentType"] == "bond":
1847                    view["stat"]["Bonds"].append(statData)
1848
1849                elif item["instrumentType"] == "etf":
1850                    view["stat"]["Etfs"].append(statData)
1851
1852                elif item["instrumentType"] == "Futures":
1853                    view["stat"]["Futures"].append(statData)
1854
1855                else:
1856                    continue
1857
1858        # total changes in Russian Ruble:
1859        view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]])  # available RUB without other currencies
1860        view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0.
1861        startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100)
1862        view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost
1863        view["stat"]["funds"]["rub"] = {
1864            "total": view["stat"]["availableRUB"],
1865            "totalCostRUB": view["stat"]["availableRUB"],
1866            "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1867            "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1868        }
1869
1870        # --- pending orders sector data:
1871        uniquePendingOrders = []
1872        uniquePendingOrdersFIGIs = []
1873        for item in view["raw"]["orders"]:
1874            if item["figi"] not in uniquePendingOrdersFIGIs:
1875                uniquePendingOrdersFIGIs.append(item["figi"])
1876                uniquePendingOrders.append(item)
1877
1878        for item in uniquePendingOrders:
1879            self.figi = item["figi"]
1880            instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI
1881
1882            if instrument:
1883                action = TKS_ORDER_DIRECTIONS[item["direction"]]
1884                orderType = TKS_ORDER_TYPES[item["orderType"]]
1885                orderState = TKS_ORDER_STATES[item["executionReportStatus"]]
1886                orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1887
1888                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1889                if item["direction"] == "ORDER_DIRECTION_BUY":
1890                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1891
1892                else:
1893                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1894
1895                # requested price for order execution:
1896                target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"])
1897
1898                # necessary changes in percent to reach target from current price:
1899                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1900
1901                view["stat"]["orders"].append({
1902                    "orderID": item["orderId"],  # orderId number parameter of current order
1903                    "figi": item["figi"],  # FIGI identification
1904                    "ticker": instrument["ticker"],  # ticker name by FIGI
1905                    "lotsRequested": item["lotsRequested"],  # requested lots value
1906                    "lotsExecuted": item["lotsExecuted"],  # how many lots are executed
1907                    "currentPrice": lastPrice,  # current instrument's price for defined action
1908                    "targetPrice": target,  # requested price for order execution in base currency
1909                    "baseCurrencyName": item["initialSecurityPrice"]["currency"],  # name of base currency
1910                    "percentChanges": changes,  # changes in percent to target from current price
1911                    "currency": item["currency"],  # instrument's currency name
1912                    "action": action,  # sell / buy / Unknown from TKS_ORDER_DIRECTIONS
1913                    "type": orderType,  # type of order from TKS_ORDER_TYPES
1914                    "status": orderState,  # order status from TKS_ORDER_STATES
1915                    "date": orderDate,  # string with order date and time from UTC format (without nano seconds part)
1916                })
1917
1918        # --- stop orders sector data:
1919        uniqueStopOrders = []
1920        uniqueStopOrdersFIGIs = []
1921        for item in view["raw"]["stopOrders"]:
1922            if item["figi"] not in uniqueStopOrdersFIGIs:
1923                uniqueStopOrdersFIGIs.append(item["figi"])
1924                uniqueStopOrders.append(item)
1925
1926        for item in uniqueStopOrders:
1927            self.figi = item["figi"]
1928            instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI
1929
1930            if instrument:
1931                action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]]
1932                orderType = TKS_STOP_ORDER_TYPES[item["orderType"]]
1933                createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1934
1935                # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order
1936                if "expirationTime" in item.keys():
1937                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"]
1938                    expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0]
1939
1940                else:
1941                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"]
1942                    expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"]
1943
1944                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1945                if item["direction"] == "STOP_ORDER_DIRECTION_BUY":
1946                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1947
1948                else:
1949                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1950
1951                # requested price when stop-order executed:
1952                target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"])
1953
1954                # price for limit-order, set up when stop-order executed:
1955                limit = NanoToFloat(item["price"]["units"], item["price"]["nano"])
1956
1957                # necessary changes in percent to reach target from current price:
1958                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1959
1960                view["stat"]["stopOrders"].append({
1961                    "orderID": item["stopOrderId"],  # stopOrderId number parameter of current stop-order
1962                    "figi": item["figi"],  # FIGI identification
1963                    "ticker": instrument["ticker"],  # ticker name by FIGI
1964                    "lotsRequested": item["lotsRequested"],  # requested lots value
1965                    "currentPrice": lastPrice,  # current instrument's price for defined action
1966                    "targetPrice": target,  # requested price for stop-order execution in base currency
1967                    "limitPrice": limit,  # price for limit-order, set up when stop-order executed, 0 if market order
1968                    "baseCurrencyName": item["stopPrice"]["currency"],  # name of base currency
1969                    "percentChanges": changes,  # changes in percent to target from current price
1970                    "currency": item["currency"],  # instrument's currency name
1971                    "action": action,  # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS
1972                    "type": orderType,  # type of order from TKS_STOP_ORDER_TYPES
1973                    "expType": expType,  # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES
1974                    "createDate": createDate,  # string with created order date and time from UTC format (without nano seconds part)
1975                    "expDate": expDate,  # string with expiration order date and time from UTC format (without nano seconds part)
1976                })
1977
1978        # --- calculating data for analytics section:
1979        # portfolio distribution by assets:
1980        view["analytics"]["distrByAssets"] = {
1981            "Ruble": {
1982                "uniques": 1,
1983                "cost": view["stat"]["availableRUB"],
1984                "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1985            },
1986            "Currencies": {
1987                "uniques": len(view["stat"]["Currencies"]),  # all foreign currencies without RUB
1988                "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"],
1989                "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1990            },
1991            "Shares": {
1992                "uniques": len(view["stat"]["Shares"]),
1993                "cost": view["stat"]["sharesCostRUB"],
1994                "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1995            },
1996            "Bonds": {
1997                "uniques": len(view["stat"]["Bonds"]),
1998                "cost": view["stat"]["bondsCostRUB"],
1999                "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2000            },
2001            "Etfs": {
2002                "uniques": len(view["stat"]["Etfs"]),
2003                "cost": view["stat"]["etfsCostRUB"],
2004                "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2005            },
2006            "Futures": {
2007                "uniques": len(view["stat"]["Futures"]),
2008                "cost": view["stat"]["futuresCostRUB"],
2009                "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2010            },
2011        }
2012
2013        # portfolio distribution by companies:
2014        view["analytics"]["distrByCompanies"]["All money cash"] = {
2015            "ticker": "",
2016            "cost": view["stat"]["allCurrenciesCostRUB"],
2017            "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2018        }
2019        view["analytics"]["distrByCompanies"].update(byComp)
2020
2021        # portfolio distribution by sectors:
2022        view["analytics"]["distrBySectors"]["All money cash"] = {
2023            "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"],
2024            "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"],
2025        }
2026        view["analytics"]["distrBySectors"].update(bySect)
2027
2028        # portfolio distribution by currencies:
2029        if "rub" not in view["analytics"]["distrByCurrencies"].keys():
2030            uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!")
2031            view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0}
2032
2033        view["analytics"]["distrByCurrencies"].update(byCurr)
2034        view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2035        view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2036
2037        # portfolio distribution by countries:
2038        if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys():
2039            uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!")
2040            view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0}
2041
2042        view["analytics"]["distrByCountries"].update(byCountry)
2043        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2044        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2045
2046        # --- Prepare text statistics overview in human-readable:
2047        if show:
2048            # Whatever the value `details`, header not changes:
2049            info = [
2050                "# Client's portfolio\n\n",
2051                "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
2052                "* **Account ID:** [{}]\n".format(self.accountId),
2053            ]
2054
2055            if details in ["full", "positions", "digest"]:
2056                info.extend([
2057                    "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2058                    "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format(
2059                        "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2060                        view["stat"]["totalChangesRUB"],
2061                        "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2062                        view["stat"]["totalChangesPercentRUB"],
2063                    ),
2064                ])
2065
2066            if details in ["full", "positions"]:
2067                info.extend([
2068                    "## Open positions\n\n",
2069                    "| Ticker [FIGI]               | Volume (blocked)                | Lots     | Curr. price  | Avg. price   | Current volume cost | Profit (%)                   |\n",
2070                    "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n",
2071                    "| Ruble                       | {:>31} |          |              |              |                     |                              |\n".format(
2072                        "{:.2f} ({:.2f}) rub".format(
2073                            view["stat"]["availableRUB"],
2074                            view["stat"]["blockedRUB"],
2075                        )
2076                    )
2077                ])
2078
2079                def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list:
2080                    return [
2081                        "|                             |                                 |          |              |              |                     |                              |\n",
2082                        "| {:<27} |                                 |          |              |              | {:>19} |                              |\n".format(
2083                            noTradeStr if noTradeStr else typeStr,
2084                            "" if noTradeStr else "{:.2f} RUB".format(CostRUB),
2085                        ),
2086                    ]
2087
2088                def _InfoStr(data: dict, showCurrencyName: bool = False) -> str:
2089                    return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format(
2090                        "{} [{}]".format(data["ticker"], data["figi"]),
2091                        "{:.2f} ({:.2f}) {}".format(
2092                            data["volume"],
2093                            data["blocked"],
2094                            data["currency"],
2095                        ) if showCurrencyName else "{:.0f} ({:.0f})".format(
2096                            data["volume"],
2097                            data["blocked"],
2098                        ),
2099                        "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]),
2100                        "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a",
2101                        "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a",
2102                        "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]),
2103                        "{}{:.2f} {} ({}{:.2f}%)".format(
2104                            "+" if data["profit"] > 0 else "",
2105                            data["profit"], data["baseCurrencyName"],
2106                            "+" if data["percentProfit"] > 0 else "",
2107                            data["percentProfit"],
2108                        ),
2109                    )
2110
2111                # --- Show currencies section:
2112                if view["stat"]["Currencies"]:
2113                    info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**"))
2114                    for item in view["stat"]["Currencies"]:
2115                        info.append(_InfoStr(item, showCurrencyName=True))
2116
2117                else:
2118                    info.extend(_SplitStr(noTradeStr="**Currencies:** no trades"))
2119
2120                # --- Show shares section:
2121                if view["stat"]["Shares"]:
2122                    info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**"))
2123
2124                    for item in view["stat"]["Shares"]:
2125                        info.append(_InfoStr(item))
2126
2127                else:
2128                    info.extend(_SplitStr(noTradeStr="**Shares:** no trades"))
2129
2130                # --- Show bonds section:
2131                if view["stat"]["Bonds"]:
2132                    info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**"))
2133
2134                    for item in view["stat"]["Bonds"]:
2135                        info.append(_InfoStr(item))
2136
2137                else:
2138                    info.extend(_SplitStr(noTradeStr="**Bonds:** no trades"))
2139
2140                # --- Show etfs section:
2141                if view["stat"]["Etfs"]:
2142                    info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**"))
2143
2144                    for item in view["stat"]["Etfs"]:
2145                        info.append(_InfoStr(item))
2146
2147                else:
2148                    info.extend(_SplitStr(noTradeStr="**Etfs:** no trades"))
2149
2150                # --- Show futures section:
2151                if view["stat"]["Futures"]:
2152                    info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**"))
2153
2154                    for item in view["stat"]["Futures"]:
2155                        info.append(_InfoStr(item))
2156
2157                else:
2158                    info.extend(_SplitStr(noTradeStr="**Futures:** no trades"))
2159
2160            if details in ["full", "orders"]:
2161                # --- Show pending orders section:
2162                if view["stat"]["orders"]:
2163                    info.extend([
2164                        "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])),
2165                        "\n| Ticker [FIGI]               | Order ID       | Lots (exec.) | Current price (% delta) | Target price  | Action    | Type      | Create date (UTC)       |\n",
2166                        "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n",
2167                    ])
2168
2169                    for item in view["stat"]["orders"]:
2170                        info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format(
2171                            "{} [{}]".format(item["ticker"], item["figi"]),
2172                            item["orderID"],
2173                            "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]),
2174                            "{} {} ({}{:.2f}%)".format(
2175                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2176                                item["baseCurrencyName"],
2177                                "+" if item["percentChanges"] > 0 else "",
2178                                float(item["percentChanges"]),
2179                            ),
2180                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2181                            item["action"],
2182                            item["type"],
2183                            item["date"],
2184                        ))
2185
2186                else:
2187                    info.append("\n## Total pending limit-orders: 0\n")
2188
2189                # --- Show stop orders section:
2190                if view["stat"]["stopOrders"]:
2191                    info.extend([
2192                        "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])),
2193                        "\n| Ticker [FIGI]               | Stop order ID                        | Lots   | Current price (% delta) | Target price  | Limit price   | Action    | Type        | Expire type  | Create date (UTC)   | Expiration (UTC)    |\n",
2194                        "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n",
2195                    ])
2196
2197                    for item in view["stat"]["stopOrders"]:
2198                        info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format(
2199                            "{} [{}]".format(item["ticker"], item["figi"]),
2200                            item["orderID"],
2201                            item["lotsRequested"],
2202                            "{} {} ({}{:.2f}%)".format(
2203                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2204                                item["baseCurrencyName"],
2205                                "+" if item["percentChanges"] > 0 else "",
2206                                float(item["percentChanges"]),
2207                            ),
2208                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2209                            "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"],
2210                            item["action"],
2211                            item["type"],
2212                            item["expType"],
2213                            item["createDate"],
2214                            item["expDate"],
2215                        ))
2216
2217                else:
2218                    info.append("\n## Total stop-orders: 0\n")
2219
2220            if details in ["full", "analytics"]:
2221                # -- Show analytics section:
2222                if view["stat"]["portfolioCostRUB"] > 0:
2223                    info.extend([
2224                        "\n# Analytics\n"
2225                        "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2226                        "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format(
2227                            "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2228                            view["stat"]["totalChangesRUB"],
2229                            "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2230                            view["stat"]["totalChangesPercentRUB"],
2231                        ),
2232                        "\n## Portfolio distribution by assets\n"
2233                        "\n| Type       | Uniques | Percent | Current cost       |\n",
2234                        "|------------|---------|---------|--------------------|\n",
2235                    ])
2236
2237                    for key in view["analytics"]["distrByAssets"].keys():
2238                        if view["analytics"]["distrByAssets"][key]["cost"] > 0:
2239                            info.append("| {:<10} | {:<7} | {:<7} | {:<18} |\n".format(
2240                                key,
2241                                view["analytics"]["distrByAssets"][key]["uniques"],
2242                                "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]),
2243                                "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]),
2244                            ))
2245
2246                    maxLenNames = 3 + max([len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"]) for company in view["analytics"]["distrByCompanies"].keys()])
2247                    info.extend([
2248                        "\n## Portfolio distribution by companies\n"
2249                        "\n| Company{} | Percent | Current cost       |\n".format(" " * (maxLenNames - 7)),
2250                        "|--------{}-|---------|--------------------|\n".format("-" * (maxLenNames - 7)),
2251                    ])
2252
2253                    for company in view["analytics"]["distrByCompanies"].keys():
2254                        if view["analytics"]["distrByCompanies"][company]["cost"] > 0:
2255                            nameLen = len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"])
2256                            info.append("| {} | {:<7} | {:<18} |\n".format(
2257                                "{}{}{}".format(
2258                                    "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "",
2259                                    company,
2260                                    "" if nameLen == maxLenNames else "{}".format(" " * (maxLenNames - nameLen - 3) if view["analytics"]["distrByCompanies"][company]["ticker"] else " " * (maxLenNames - nameLen)),
2261                                ),
2262                                "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]),
2263                                "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]),
2264                            ))
2265
2266                    maxLenSectors = max([len(sector) for sector in view["analytics"]["distrBySectors"].keys()])
2267                    info.extend([
2268                        "\n## Portfolio distribution by sectors\n"
2269                        "\n| Sector{} | Percent | Current cost       |\n".format(" " * (maxLenSectors - 6)),
2270                        "|-------{}-|---------|--------------------|\n".format("-" * (maxLenSectors - 6)),
2271                    ])
2272
2273                    for sector in view["analytics"]["distrBySectors"].keys():
2274                        if view["analytics"]["distrBySectors"][sector]["cost"] > 0:
2275                            info.append("| {}{} | {:<7} | {:<18} |\n".format(
2276                                sector,
2277                                "" if len(sector) == maxLenSectors else " " * (maxLenSectors - len(sector)),
2278                                "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]),
2279                                "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]),
2280                            ))
2281
2282                    maxLenMoney = 3 + max([len(currency) + len(view["analytics"]["distrByCurrencies"][currency]["name"]) for currency in view["analytics"]["distrByCurrencies"].keys()])
2283                    info.extend([
2284                        "\n## Portfolio distribution by currencies\n"
2285                        "\n| Instruments currencies{} | Percent | Current cost       |\n".format(" " * (maxLenMoney - 22)),
2286                        "|-----------------------{}-|---------|--------------------|\n".format("-" * (maxLenMoney - 22)),
2287                    ])
2288
2289                    for curr in view["analytics"]["distrByCurrencies"].keys():
2290                        if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0:
2291                            nameLen = 3 + len(curr) + len(view["analytics"]["distrByCurrencies"][curr]["name"])
2292                            info.append("| {} | {:<7} | {:<18} |\n".format(
2293                                "[{}] {}{}".format(
2294                                    curr,
2295                                    view["analytics"]["distrByCurrencies"][curr]["name"],
2296                                    "" if nameLen == maxLenMoney else " " * (maxLenMoney - nameLen),
2297                                ),
2298                                "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]),
2299                                "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]),
2300                            ))
2301
2302                    maxLenCountry = max(17, max([len(country) for country in view["analytics"]["distrByCountries"].keys()]))
2303                    info.extend([
2304                        "\n## Portfolio distribution by countries\n"
2305                        "\n| Assets by country{} | Percent | Current cost       |\n".format(" " * (maxLenCountry - 17)),
2306                        "|------------------{}-|---------|--------------------|\n".format("-" * (maxLenCountry - 17)),
2307                    ])
2308
2309                    for country in view["analytics"]["distrByCountries"].keys():
2310                        if view["analytics"]["distrByCountries"][country]["cost"] > 0:
2311                            nameLen = len(country)
2312                            info.append("| {} | {:<7} | {:<18} |\n".format(
2313                                "{}{}".format(
2314                                    country,
2315                                    "" if nameLen == maxLenCountry else " " * (maxLenCountry - nameLen),
2316                                ),
2317                                "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]),
2318                                "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]),
2319                            ))
2320
2321            infoText = "".join(info)
2322
2323            uLogger.info(infoText)
2324
2325            if details == "full" and self.overviewFile:
2326                filename = self.overviewFile
2327
2328            elif details == "digest" and self.overviewDigestFile:
2329                filename = self.overviewDigestFile
2330
2331            elif details == "positions" and self.overviewPositionsFile:
2332                filename = self.overviewPositionsFile
2333
2334            elif details == "orders" and self.overviewOrdersFile:
2335                filename = self.overviewOrdersFile
2336
2337            elif details == "analytics" and self.overviewAnalyticsFile:
2338                filename = self.overviewAnalyticsFile
2339
2340            else:
2341                filename = ""
2342
2343            if filename:
2344                with open(filename, "w", encoding="UTF-8") as fH:
2345                    fH.write(infoText)
2346
2347                uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename)))
2348
2349        return view

Get portfolio: all open positions, orders and some statistics for current accountId. If overviewFile, overviewDigestFile, overviewPositionsFile, overviewOrdersFile, overviewAnalyticsFile are defined then also save information to file.

WARNING! It is not recommended to run this method too many times in a loop! The server receives many requests about the state of the portfolio, and then, based on the received data, a large number of calculation and statistics are collected.

Parameters
  • show: if False then only dictionary returns, if True then show more debug information.
  • details: how detailed should the information be? You should specify one of strings: full - shows full available information about portfolio status (by default), positions - shows only open positions, digest - show a short digest of the portfolio status, analytics - shows only the analytics section and the distribution of the portfolio by various categories, orders - shows only sections of open limits and stop orders.
Returns

dictionary with client's raw portfolio and some statistics.

def Deals( self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple:
2351    def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple:
2352        """
2353        Returns history operations between two given dates for current `accountId`.
2354        If `reportFile` string is not empty then also save human-readable report.
2355        Shows some statistical data of closed positions.
2356
2357        :param start: see docstring in `GetDatesAsString()` method
2358        :param end: see docstring in `GetDatesAsString()` method
2359        :param show: if `True` then also prints all records to the console.
2360        :param showCancelled: if `False` then remove information about cancelled operations from the deals report.
2361        :return: original list of dictionaries with history of deals records from API ("operations" key):
2362                 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2363                 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2364        """
2365        if self.accountId is None or not self.accountId:
2366            uLogger.error("Variable `accountId` must be defined for using this method!")
2367            raise Exception("Account ID required")
2368
2369        startDate, endDate = GetDatesAsString(start, end)  # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2370
2371        uLogger.debug("Requesting history of a client's operations. Wait, please...")
2372
2373        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2374        dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations"
2375        self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate})
2376        ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"]  # list of dict: operations returns by broker
2377        customStat = {}  # custom statistics in additional to responseJSON
2378
2379        # --- output report in human-readable format:
2380        if show or self.reportFile:
2381            splitLine1 = "|                            |                               |                              |                      |                        |\n"  # Summary section
2382            splitLine2 = "|                     |              |              |            |           |                 |            |                                                                    |\n"  # Operations section
2383            nextDay = ""
2384
2385            info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])]
2386
2387            if len(ops) > 0:
2388                customStat = {
2389                    "opsCount": 0,  # total operations count
2390                    "buyCount": 0,  # buy operations
2391                    "sellCount": 0,  # sell operations
2392                    "buyTotal": {"rub": 0.},  # Buy sums in different currencies
2393                    "sellTotal": {"rub": 0.},  # Sell sums in different currencies
2394                    "payIn": {"rub": 0.},  # Deposit brokerage account
2395                    "payOut": {"rub": 0.},  # Withdrawals
2396                    "divs": {"rub": 0.},  # Dividends income
2397                    "coupons": {"rub": 0.},  # Coupon's income
2398                    "brokerCom": {"rub": 0.},  # Service commissions
2399                    "serviceCom": {"rub": 0.},  # Service commissions
2400                    "marginCom": {"rub": 0.},  # Margin commissions
2401                    "allTaxes": {"rub": 0.},  # Sum of withholding taxes and corrections
2402                }
2403
2404                # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES:
2405                for item in ops:
2406                    if item["state"] == "OPERATION_STATE_EXECUTED":
2407                        payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2408
2409                        # count buy operations:
2410                        if "_BUY" in item["operationType"]:
2411                            customStat["buyCount"] += 1
2412
2413                            if item["payment"]["currency"] in customStat["buyTotal"].keys():
2414                                customStat["buyTotal"][item["payment"]["currency"]] += payment
2415
2416                            else:
2417                                customStat["buyTotal"][item["payment"]["currency"]] = payment
2418
2419                        # count sell operations:
2420                        elif "_SELL" in item["operationType"]:
2421                            customStat["sellCount"] += 1
2422
2423                            if item["payment"]["currency"] in customStat["sellTotal"].keys():
2424                                customStat["sellTotal"][item["payment"]["currency"]] += payment
2425
2426                            else:
2427                                customStat["sellTotal"][item["payment"]["currency"]] = payment
2428
2429                        # count incoming operations:
2430                        elif item["operationType"] in ["OPERATION_TYPE_INPUT"]:
2431                            if item["payment"]["currency"] in customStat["payIn"].keys():
2432                                customStat["payIn"][item["payment"]["currency"]] += payment
2433
2434                            else:
2435                                customStat["payIn"][item["payment"]["currency"]] = payment
2436
2437                        # count withdrawals operations:
2438                        elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]:
2439                            if item["payment"]["currency"] in customStat["payOut"].keys():
2440                                customStat["payOut"][item["payment"]["currency"]] += payment
2441
2442                            else:
2443                                customStat["payOut"][item["payment"]["currency"]] = payment
2444
2445                        # count dividends income:
2446                        elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]:
2447                            if item["payment"]["currency"] in customStat["divs"].keys():
2448                                customStat["divs"][item["payment"]["currency"]] += payment
2449
2450                            else:
2451                                customStat["divs"][item["payment"]["currency"]] = payment
2452
2453                        # count coupon's income:
2454                        elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]:
2455                            if item["payment"]["currency"] in customStat["coupons"].keys():
2456                                customStat["coupons"][item["payment"]["currency"]] += payment
2457
2458                            else:
2459                                customStat["coupons"][item["payment"]["currency"]] = payment
2460
2461                        # count broker commissions:
2462                        elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]:
2463                            if item["payment"]["currency"] in customStat["brokerCom"].keys():
2464                                customStat["brokerCom"][item["payment"]["currency"]] += payment
2465
2466                            else:
2467                                customStat["brokerCom"][item["payment"]["currency"]] = payment
2468
2469                        # count service commissions:
2470                        elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]:
2471                            if item["payment"]["currency"] in customStat["serviceCom"].keys():
2472                                customStat["serviceCom"][item["payment"]["currency"]] += payment
2473
2474                            else:
2475                                customStat["serviceCom"][item["payment"]["currency"]] = payment
2476
2477                        # count margin commissions:
2478                        elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]:
2479                            if item["payment"]["currency"] in customStat["marginCom"].keys():
2480                                customStat["marginCom"][item["payment"]["currency"]] += payment
2481
2482                            else:
2483                                customStat["marginCom"][item["payment"]["currency"]] = payment
2484
2485                        # count withholding taxes:
2486                        elif "_TAX" in item["operationType"]:
2487                            if item["payment"]["currency"] in customStat["allTaxes"].keys():
2488                                customStat["allTaxes"][item["payment"]["currency"]] += payment
2489
2490                            else:
2491                                customStat["allTaxes"][item["payment"]["currency"]] = payment
2492
2493                        else:
2494                            continue
2495
2496                customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"]
2497
2498                # --- view "Actions" lines:
2499                info.extend([
2500                    "| 1                          | 2                             | 3                            | 4                    | 5                      |\n",
2501                    "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n",
2502                    "| **Actions:**               | Trades: {:<21} | Trading volumes:             |                      |                        |\n".format(customStat["opsCount"]),
2503                    "|                            |   Buy: {:<22} | {:<28} |                      |                        |\n".format(
2504                        "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2505                        "  rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else "  —",
2506                    ),
2507                    "|                            |   Sell: {:<21} | {:<28} |                      |                        |\n".format(
2508                        "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2509                        "  rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else "  —",
2510                    ),
2511                ])
2512
2513                opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys()))))
2514                for key in opsKeys:
2515                    if key == "rub":
2516                        continue
2517
2518                    info.extend([
2519                        "|                            |                               | {:<28} |                      |                        |\n".format(
2520                            "  {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0)
2521                        ),
2522                        "|                            |                               | {:<28} |                      |                        |\n".format(
2523                            "  {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0)
2524                        ),
2525                    ])
2526
2527                info.append(splitLine1)
2528
2529                def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str:
2530                    return "|                            | {:<29} | {:<28} | {:<20} | {:<22} |\n".format(
2531                            "  {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else "  —",
2532                            "  {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else "  —",
2533                            "  {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else "  —",
2534                            "  {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else "  —",
2535                    )
2536
2537                # --- view "Payments" lines:
2538                info.append("| **Payments:**              | Deposit on broker account:    | Withdrawals:                 | Dividends income:    | Coupons income:        |\n")
2539                paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys()))))
2540
2541                for key in paymentsKeys:
2542                    info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key))
2543
2544                info.append(splitLine1)
2545
2546                # --- view "Commissions and taxes" lines:
2547                info.append("| **Commissions and taxes:** | Broker commissions:           | Service commissions:         | Margin commissions:  | All taxes/corrections: |\n")
2548                comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys()))))
2549
2550                for key in comKeys:
2551                    info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key))
2552
2553                info.append(splitLine1)
2554
2555                info.extend([
2556                    "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"),
2557                    "| Date and time       | FIGI         | Ticker       | Asset      | Value     | Payment         | Status     | Operation type                                                     |\n",
2558                    "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n",
2559                ])
2560
2561            else:
2562                info.append("Broker returned no operations during this period\n")
2563
2564            # --- view "Operations" section:
2565            for item in ops:
2566                if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]:
2567                    continue
2568
2569                else:
2570                    self.figi = item["figi"] if item["figi"] else ""
2571                    payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2572                    instrument = self.SearchByFIGI(requestPrice=False) if self.figi else {}
2573
2574                    # group of deals during one day:
2575                    if nextDay and item["date"].split("T")[0] != nextDay:
2576                        info.append(splitLine2)
2577                        nextDay = ""
2578
2579                    else:
2580                        nextDay = item["date"].split("T")[0]  # saving current day for splitting
2581
2582                    info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format(
2583                        item["date"].replace("T", " ").replace("Z", "").split(".")[0],
2584                        self.figi if self.figi else "—",
2585                        instrument["ticker"] if instrument else "—",
2586                        instrument["type"] if instrument else "—",
2587                        item["quantity"] if int(item["quantity"]) > 0 else "—",
2588                        "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—",
2589                        TKS_OPERATION_STATES[item["state"]],
2590                        TKS_OPERATION_TYPES[item["operationType"]],
2591                    ))
2592
2593            infoText = "".join(info)
2594
2595            if show:
2596                uLogger.info(infoText)
2597
2598            if self.reportFile:
2599                with open(self.reportFile, "w", encoding="UTF-8") as fH:
2600                    fH.write(infoText)
2601
2602                uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile)))
2603
2604        return ops, customStat

Returns history operations between two given dates for current accountId. If reportFile string is not empty then also save human-readable report. Shows some statistical data of closed positions.

Parameters
  • start: see docstring in GetDatesAsString() method
  • end: see docstring in GetDatesAsString() method
  • show: if True then also prints all records to the console.
  • showCancelled: if False then remove information about cancelled operations from the deals report.
Returns

original list of dictionaries with history of deals records from API ("operations" key): https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.

def History( self, start: str = None, end: str = None, interval: str = 'hour', onlyMissing: bool = False, csvSep: str = ',', show: bool = False) -> pandas.core.frame.DataFrame:
2606    def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame:
2607        """
2608        This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id).
2609
2610        History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`.
2611        Warning! Broker server used ISO UTC time by default.
2612
2613        If `historyFile` is not `None` then method save history to file, otherwise return only pandas dataframe.
2614        Also, `historyFile` used to update history with `onlyMissing` parameter.
2615
2616        See also: `LoadHistory()` and `ShowHistoryChart()` methods.
2617
2618        :param start: see docstring in `GetDatesAsString()` method.
2619        :param end: see docstring in `GetDatesAsString()` method.
2620        :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`,
2621                         `"hour"`, `"day"`. Default: `"hour"`.
2622        :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`.
2623                            False by default. Warning! History appends only from last candle to current time
2624                            with always update last candle!
2625        :param csvSep: separator if csv-file is used, `,` by default.
2626        :param show: if `True` then also prints pandas dataframe to the console.
2627        :return: pandas dataframe with prices history. Headers of columns are defined by default:
2628                 `["date", "time", "open", "high", "low", "close", "volume"]`.
2629        """
2630        strStartDate, strEndDate = GetDatesAsString(start, end)  # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2631        headers = ["date", "time", "open", "high", "low", "close", "volume"]  # sequence and names of column headers
2632        history = None  # empty pandas object for history
2633
2634        if interval not in TKS_CANDLE_INTERVALS.keys():
2635            uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.")
2636            raise Exception("Incorrect value")
2637
2638        if not (self.ticker or self.figi):
2639            uLogger.error("Ticker or FIGI must be defined!")
2640            raise Exception("Ticker or FIGI required")
2641
2642        if self.ticker and not self.figi:
2643            instrumentByTicker = self.SearchByTicker(requestPrice=False, debug=False)
2644            self.figi = instrumentByTicker["figi"] if instrumentByTicker else ""
2645
2646        if self.figi and not self.ticker:
2647            instrumentByFIGI = self.SearchByFIGI(requestPrice=False, debug=False)
2648            self.ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else ""
2649
2650        dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from start time string
2651        dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from end time string
2652        if interval.lower() != "day":
2653            dtEnd += timedelta(seconds=1)  # adds 1 sec for requests, because day end returned by `GetDatesAsString()` as 23:59:59
2654
2655        delta = dtEnd - dtStart  # current UTC time minus last time in file
2656        deltaMinutes = delta.days * 1440 + delta.seconds // 60  # minutes between start and end dates
2657
2658        # calculate history length in candles:
2659        length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1]
2660        if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0:
2661            length += 1  # to avoid fraction time
2662
2663        # calculate data blocks count:
2664        blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2]
2665
2666        uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end))
2667        uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate))
2668        uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval))
2669        uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2]))
2670        uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self.ticker, self.figi))
2671
2672        tempOld = None  # pandas object for old history, if --only-missing key present
2673        lastTime = None  # datetime object of last old candle in file
2674
2675        if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile):
2676            uLogger.debug("--only-missing key present, add only last missing candles...")
2677            uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile)))
2678
2679            tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers)
2680
2681            tempOld["date"] = pd.to_datetime(tempOld["date"])  # load date "as is"
2682            tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d")  # convert date to string
2683            tempOld["time"] = pd.to_datetime(tempOld["time"])  # load time "as is"
2684            tempOld["time"] = tempOld["time"].dt.strftime("%H:%M")  # convert time to string
2685
2686            # get last datetime object from last string in file or minus 1 delta if file is empty:
2687            if len(tempOld) > 0:
2688                lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2689
2690            else:
2691                lastTime = dtEnd - timedelta(days=1)  # history file is empty, so last date set at -1 day
2692
2693            tempOld = tempOld[:-1]  # always remove last old candle because it may be incompletely at the current time
2694
2695        responseJSONs = []  # raw history blocks of data
2696
2697        blockEnd = dtEnd
2698        for item in range(blocks):
2699            tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2]
2700            blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail)
2701
2702            uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format(
2703                item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2704            ))
2705
2706            if blockStart == blockEnd:
2707                uLogger.debug("Skipped this zero-length block...")
2708
2709            else:
2710                # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles
2711                historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles"
2712                self.body = str({
2713                    "figi": self.figi,
2714                    "from": blockStart.strftime(TKS_DATE_TIME_FORMAT),
2715                    "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2716                    "interval": TKS_CANDLE_INTERVALS[interval][0]
2717                })
2718                responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1, debug=False)
2719
2720                if "code" in responseJSON.keys():
2721                    uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks))
2722
2723                else:
2724                    if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1:
2725                        responseJSON["candles"] = responseJSON["candles"][:-1]  # removes last candle for "yesterday" request
2726
2727                    responseJSONs = responseJSON["candles"] + responseJSONs  # add more old history behind newest dates
2728
2729            blockEnd = blockStart
2730
2731        printCount = len(responseJSONs)  # candles to show in console
2732        if responseJSONs:
2733            tempHistory = pd.DataFrame(
2734                data={
2735                    "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2736                    "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2737                    "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs],
2738                    "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs],
2739                    "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs],
2740                    "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs],
2741                    "volume": [int(item["volume"]) for item in responseJSONs],
2742                },
2743                index=range(len(responseJSONs)),
2744                columns=["date", "time", "open", "high", "low", "close", "volume"],
2745            )
2746            tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d")
2747            tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M")
2748
2749            # append only newest candles to old history if --only-missing key present:
2750            if onlyMissing and tempOld is not None and lastTime is not None:
2751                index = 0  # find start index in tempHistory data:
2752
2753                for i, item in tempHistory.iterrows():
2754                    curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2755
2756                    if curTime == lastTime:
2757                        uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
2758                        index = i
2759                        printCount = index + 1
2760                        break
2761
2762                history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True)
2763
2764            else:
2765                history = tempHistory  # if no `--only-missing` key then load full data from server
2766
2767            uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False)))
2768
2769        if history is not None and not history.empty:
2770            if show:
2771                uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format(
2772                    strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]),
2773                    pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False),
2774                ))
2775
2776        else:
2777            uLogger.warning("Received an empty candles history!")
2778
2779        if self.historyFile is not None:
2780            if history is not None and not history.empty:
2781                history.to_csv(self.historyFile, sep=csvSep, index=False, header=None)
2782                uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self.ticker, self.figi, interval, os.path.abspath(self.historyFile)))
2783
2784            else:
2785                uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile)))
2786
2787        else:
2788            uLogger.debug("--output key is not defined. Parsed history file not saved to file, only pandas dataframe returns.")
2789
2790        return history

This method returns last history candles of the current instrument defined by ticker or figi (FIGI id).

History returned between two given dates: start and end. Minimum requested date in the past is 1970-01-01. Warning! Broker server used ISO UTC time by default.

If historyFile is not None then method save history to file, otherwise return only pandas dataframe. Also, historyFile used to update history with onlyMissing parameter.

See also: LoadHistory() and ShowHistoryChart() methods.

Parameters
  • start: see docstring in GetDatesAsString() method.
  • end: see docstring in GetDatesAsString() method.
  • interval: this is a candle interval. Current available values are "1min", "5min", "15min", "hour", "day". Default: "hour".
  • onlyMissing: if True then add only last missing candles, do not request all history length from start. False by default. Warning! History appends only from last candle to current time with always update last candle!
  • csvSep: separator if csv-file is used, , by default.
  • show: if True then also prints pandas dataframe to the console.
Returns

pandas dataframe with prices history. Headers of columns are defined by default: ["date", "time", "open", "high", "low", "close", "volume"].

def LoadHistory(self, filePath: str) -> pandas.core.frame.DataFrame:
2792    def LoadHistory(self, filePath: str) -> pd.DataFrame:
2793        """
2794        Load candles history from csv-file and return pandas dataframe object.
2795
2796        See also: `History()` and `ShowHistoryChart()` methods.
2797
2798        :param filePath: path to csv-file to open.
2799        """
2800        loadedHistory = None  # init candles data object
2801
2802        uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...")
2803
2804        if os.path.exists(filePath):
2805            loadedHistory = self.priceModel.LoadFromFile(filePath)  # load data and get chain of candles as pandas dataframe
2806
2807            tfStr = self.priceModel.FormattedDelta(
2808                self.priceModel.timeframe,
2809                "{days} days {hours}h {minutes}m {seconds}s",
2810            ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta(
2811                self.priceModel.timeframe,
2812                "{hours}h {minutes}m {seconds}s",
2813            )
2814
2815            if loadedHistory is not None and not loadedHistory.empty:
2816                uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format(
2817                    len(loadedHistory),
2818                    tfStr,
2819                    pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)),
2820                )
2821
2822            else:
2823                uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath)))
2824
2825        else:
2826            uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath))
2827
2828        return loadedHistory

Load candles history from csv-file and return pandas dataframe object.

See also: History() and ShowHistoryChart() methods.

Parameters
  • filePath: path to csv-file to open.
def ShowHistoryChart( self, candles: Union[str, pandas.core.frame.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2830    def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2831        """
2832        Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
2833
2834        Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart.
2835        Default: `index.html` (both for interact and non-interact candlesticks chart).
2836
2837        See also: `History()` and `LoadHistory()` methods.
2838
2839        :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
2840        :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart.
2841                         See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters
2842                         If False then chain of candlesticks will render as not interactive Google Candlestick chart.
2843                         See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
2844        :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to
2845                              html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file.
2846        """
2847        if isinstance(candles, str):
2848            self.priceModel.prices = self.LoadHistory(filePath=candles)  # load candles chain from file
2849            self.priceModel.ticker = os.path.basename(candles)  # use filename as ticker name in PriceGenerator
2850
2851        elif isinstance(candles, pd.DataFrame):
2852            self.priceModel.prices = candles  # set candles chain from variable
2853            self.priceModel.ticker = self.ticker  # use current TKSBrokerAPI ticker as ticker name in PriceGenerator
2854
2855            if "datetime" not in candles.columns:
2856                self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True)  # PriceGenerator uses "datetime" column with date and time
2857
2858        else:
2859            uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!")
2860            raise Exception("Incorrect value")
2861
2862        self.priceModel.horizon = len(self.priceModel.prices)  # use length of candles data as horizon in PriceGenerator
2863
2864        if interact:
2865            uLogger.debug("Rendering interactive candles chart. Wait, please...")
2866
2867            self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2868
2869        else:
2870            uLogger.debug("Rendering non-interactive candles chart. Wait, please...")
2871
2872            self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2873
2874        uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))

Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.

Self variable htmlHistoryFile can be use as html-file name to save interaction or non-interaction chart. Default: index.html (both for interact and non-interact candlesticks chart).

See also: History() and LoadHistory() methods.

Parameters
def Trade( self, operation: str, lots: int = 1, tp: float = 0.0, sl: float = 0.0, expDate: str = 'Undefined') -> dict:
2876    def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2877        """
2878        Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response.
2879        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2880
2881        See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`.
2882
2883        :param operation: string "Buy" or "Sell".
2884        :param lots: volume, integer count of lots >= 1.
2885        :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`.
2886        :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`.
2887        :param expDate: string "Undefined" by default or local date in future,
2888                        it is a string with format `%Y-%m-%d %H:%M:%S`.
2889        :return: JSON with response from broker server.
2890        """
2891        if self.accountId is None or not self.accountId:
2892            uLogger.error("Variable `accountId` must be defined for using this method!")
2893            raise Exception("Account ID required")
2894
2895        if operation is None or not operation or operation not in ("Buy", "Sell"):
2896            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
2897            raise Exception("Incorrect value")
2898
2899        if lots is None or lots < 1:
2900            uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.")
2901            lots = 1
2902
2903        if tp is None or tp < 0:
2904            tp = 0
2905
2906        if sl is None or sl < 0:
2907            sl = 0
2908
2909        if expDate is None or not expDate:
2910            expDate = "Undefined"
2911
2912        if not (self.ticker or self.figi):
2913            uLogger.error("Ticker or FIGI must be defined!")
2914            raise Exception("Ticker or FIGI required")
2915
2916        instrument = self.SearchByTicker(requestPrice=True, debug=False) if self.ticker else self.SearchByFIGI(requestPrice=True, debug=False)
2917        self.ticker = instrument["ticker"]
2918        self.figi = instrument["figi"]
2919
2920        uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self.ticker, self.figi, lots, tp, sl, expDate))
2921
2922        openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
2923        self.body = str({
2924            "figi": self.figi,
2925            "quantity": str(lots),
2926            "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
2927            "accountId": str(self.accountId),
2928            "orderType": "ORDER_TYPE_MARKET",  # see: TKS_ORDER_TYPES
2929        })
2930        response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0, debug=False)
2931
2932        if "orderId" in response.keys():
2933            uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format(
2934                operation, response["orderId"],
2935                self.ticker, self.figi, lots,
2936                NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"],
2937                NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"],
2938                NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"],
2939            ))
2940
2941        else:
2942            uLogger.warning("Not `oK` status received! Market order not created. See full debug log or try again and open order later.")
2943
2944        if tp > 0:
2945            self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate)
2946
2947        if sl > 0:
2948            self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate)
2949
2950        return response

Universal method to create market order and make deal at the current price for current accountId. Returns JSON data with response. If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.

See also: Order() docstring. More simple methods than Trade() are Buy() and Sell().

Parameters
  • operation: string "Buy" or "Sell".
  • lots: volume, integer count of lots >= 1.
  • tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter targetPrice in self.Order().
  • sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter targetPrice in self.Order().
  • expDate: string "Undefined" by default or local date in future, it is a string with format %Y-%m-%d %H:%M:%S.
Returns

JSON with response from broker server.

def Buy( self, lots: int = 1, tp: float = 0.0, sl: float = 0.0, expDate: str = 'Undefined') -> dict:
2952    def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2953        """
2954        More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response.
2955        If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter.
2956
2957        See also: `Order()` and `Trade()` docstrings.
2958
2959        :param lots: volume, integer count of lots >= 1.
2960        :param tp: float > 0, take profit price of stop-order.
2961        :param sl: float > 0, stop loss price of stop-order.
2962        :param expDate: it's a local date in future.
2963                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
2964        :return: JSON with response from broker server.
2965        """
2966        return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)

More simple method than Trade(). Create Buy market order and make deal at the current price. Returns JSON data with response. If tp or sl > 0, then in additional will opens stop-orders with "TP" and "SL" flags for stopType parameter.

See also: Order() and Trade() docstrings.

Parameters
  • lots: volume, integer count of lots >= 1.
  • tp: float > 0, take profit price of stop-order.
  • sl: float > 0, stop loss price of stop-order.
  • expDate: it's a local date in future. String has a format like this: %Y-%m-%d %H:%M:%S.
Returns

JSON with response from broker server.

def Sell( self, lots: int = 1, tp: float = 0.0, sl: float = 0.0, expDate: str = 'Undefined') -> dict:
2968    def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2969        """
2970        More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response.
2971        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2972
2973        See also: `Order()` and `Trade()` docstrings.
2974
2975        :param lots: volume, integer count of lots >= 1.
2976        :param tp: float > 0, take profit price of stop-order.
2977        :param sl: float > 0, stop loss price of stop-order.
2978        :param expDate: it's a local date in the future.
2979                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
2980        :return: JSON with response from broker server.
2981        """
2982        return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)

More simple method than Trade(). Create Sell market order and make deal at the current price. Returns JSON data with response. If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.

See also: Order() and Trade() docstrings.

Parameters
  • lots: volume, integer count of lots >= 1.
  • tp: float > 0, take profit price of stop-order.
  • sl: float > 0, stop loss price of stop-order.
  • expDate: it's a local date in the future. String has a format like this: %Y-%m-%d %H:%M:%S.
Returns

JSON with response from broker server.

def CloseTrades(self, tickers: list, portfolio: dict = None) -> None:
2984    def CloseTrades(self, tickers: list, portfolio: dict = None) -> None:
2985        """
2986        Close position of given instruments.
2987
2988        :param tickers: tickers list of instruments that must be closed.
2989        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
2990                         This avoids unnecessary downloading data from the server.
2991        """
2992        if not tickers:
2993            uLogger.info("Tickers list is empty, nothing to close.")
2994
2995        else:
2996            if portfolio is None or not portfolio:
2997                portfolio = self.Overview(show=False)
2998
2999            allOpenedTickers = [item["ticker"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]]
3000            uLogger.debug("All opened instruments by it's tickers names: {}".format(allOpenedTickers))
3001
3002            for ticker in tickers:
3003                if ticker not in allOpenedTickers:
3004                    uLogger.warning("Instrument with ticker [{}] not in open positions list!".format(ticker))
3005                    continue
3006
3007                # search open trade info about instrument by ticker:
3008                instrument = {}
3009                for iType in TKS_INSTRUMENTS:
3010                    if instrument:
3011                        break
3012
3013                    for item in portfolio["stat"][iType]:
3014                        if item["ticker"] == ticker:
3015                            instrument = item
3016                            break
3017
3018                if instrument:
3019                    self.ticker = ticker
3020                    self.figi = instrument["figi"]
3021
3022                    uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format(
3023                        self.ticker,
3024                        self.figi,
3025                        int(instrument["volume"]),
3026                        ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "",
3027                    ))
3028
3029                    tradeLots = abs(instrument["lots"]) - instrument["blocked"]  # available volumes in lots for close operation
3030
3031                    if tradeLots > 0:
3032                        if instrument["blocked"] > 0:
3033                            uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format(
3034                                instrument["blocked"],
3035                                self.ticker,
3036                                tradeLots,
3037                            ))
3038
3039                        # if direction is "Long" then we need sell, if direction is "Short" then we need buy:
3040                        self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots)
3041
3042                    else:
3043                        uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self.ticker))

Close position of given instruments.

Parameters
  • tickers: tickers list of instruments that must be closed.
  • portfolio: pre-received dictionary with open trades, returned by Overview() method. This avoids unnecessary downloading data from the server.
def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3045    def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3046        """
3047        Close all positions of given instruments with defined type.
3048
3049        :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
3050        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3051                         This avoids unnecessary downloading data from the server.
3052        """
3053        if iType not in TKS_INSTRUMENTS:
3054            uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType))
3055
3056        else:
3057            if portfolio is None or not portfolio:
3058                portfolio = self.Overview(show=False)
3059
3060            tickers = [item["ticker"] for item in portfolio["stat"][iType]]
3061            uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers))
3062
3063            if tickers and portfolio:
3064                self.CloseTrades(tickers, portfolio)
3065
3066            else:
3067                uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))

Close all positions of given instruments with defined type.

Parameters
  • iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
  • portfolio: pre-received dictionary with open trades, returned by Overview() method. This avoids unnecessary downloading data from the server.
def Order( self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0.0, stopType: str = 'Limit', expDate: str = 'Undefined') -> dict:
3069    def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3070        """
3071        Universal method to create market or limit orders with all available parameters for current `accountId`.
3072        See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`.
3073
3074        If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above
3075        current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
3076
3077        Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell"
3078        then broker immediately open market order as you can do simple --buy or --sell operations!
3079
3080        If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell".
3081        When current price will go up or down to target price value then broker opens a limit order.
3082        Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
3083
3084        Only one attempt and no retry for opens order. If network issue occurred you can create new request.
3085
3086        :param operation: string "Buy" or "Sell".
3087        :param orderType: string "Limit" or "Stop".
3088        :param lots: volume, integer count of lots >= 1.
3089        :param targetPrice: target price > 0. This is open trade price for limit order.
3090        :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice.
3091                           Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
3092        :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types
3093                         "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3094                         Stop loss order always executed by market price.
3095        :param expDate: string "Undefined" by default or local date in future.
3096                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3097                        This date is converting to UTC format for server. This parameter only makes sense for stop-order.
3098                        A limit order has no expiration date, it lasts until the end of the trading day.
3099        :return: JSON with response from broker server.
3100        """
3101        if self.accountId is None or not self.accountId:
3102            uLogger.error("Variable `accountId` must be defined for using this method!")
3103            raise Exception("Account ID required")
3104
3105        if operation is None or not operation or operation not in ("Buy", "Sell"):
3106            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
3107            raise Exception("Incorrect value")
3108
3109        if orderType is None or not orderType or orderType not in ("Limit", "Stop"):
3110            uLogger.error("You must define order type only one of them: `Limit` or `Stop`!")
3111            raise Exception("Incorrect value")
3112
3113        if lots is None or lots < 1:
3114            uLogger.error("You must define trade volume > 0: integer count of lots!")
3115            raise Exception("Incorrect value")
3116
3117        if targetPrice is None or targetPrice <= 0:
3118            uLogger.error("Target price for limit-order must be greater than 0!")
3119            raise Exception("Incorrect value")
3120
3121        if limitPrice is None or limitPrice <= 0:
3122            limitPrice = targetPrice
3123
3124        if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"):
3125            stopType = "Limit"
3126
3127        if expDate is None or not expDate:
3128            expDate = "Undefined"
3129
3130        if not (self.ticker or self.figi):
3131            uLogger.error("Tocker or FIGI must be defined!")
3132            raise Exception("Ticker or FIGI required")
3133
3134        response = {}
3135        instrument = self.SearchByTicker(requestPrice=True, debug=False) if self.ticker else self.SearchByFIGI(requestPrice=True, debug=False)
3136        self.ticker = instrument["ticker"]
3137        self.figi = instrument["figi"]
3138
3139        if orderType == "Limit":
3140            uLogger.debug(
3141                "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format(
3142                    self.ticker, self.figi,
3143                    operation, lots, targetPrice, instrument["currency"],
3144                ))
3145
3146            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3147            self.body = str({
3148                "figi": self.figi,
3149                "quantity": str(lots),
3150                "price": FloatToNano(targetPrice),
3151                "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3152                "accountId": str(self.accountId),
3153                "orderType": "ORDER_TYPE_LIMIT",  # see: TKS_ORDER_TYPES
3154            })
3155            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0, debug=False)
3156
3157            if "orderId" in response.keys():
3158                uLogger.info(
3159                    "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format(
3160                        response["orderId"],
3161                        self.ticker, self.figi,
3162                        operation, lots, targetPrice, instrument["currency"],
3163                    ))
3164
3165                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3166                    if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]:
3167                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format(
3168                            targetPrice, instrument["currency"],
3169                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3170                        ))
3171
3172                    if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]:
3173                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format(
3174                            targetPrice, instrument["currency"],
3175                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3176                        ))
3177
3178            else:
3179                uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log or try again and open order later.")
3180
3181        if orderType == "Stop":
3182            uLogger.debug(
3183                "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format(
3184                    self.ticker, self.figi,
3185                    operation, lots,
3186                    targetPrice, instrument["currency"],
3187                    limitPrice, instrument["currency"],
3188                    stopType, expDate,
3189                ))
3190
3191            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder"
3192            expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT)
3193            stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT"
3194
3195            body = {
3196                "figi": self.figi,
3197                "quantity": str(lots),
3198                "price": FloatToNano(limitPrice),
3199                "stopPrice": FloatToNano(targetPrice),
3200                "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL",  # see: TKS_STOP_ORDER_DIRECTIONS
3201                "accountId": str(self.accountId),
3202                "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL",  # see: TKS_STOP_ORDER_EXPIRATION_TYPES
3203                "stopOrderType": stopOrderType,  # see: TKS_STOP_ORDER_TYPES
3204            }
3205
3206            if expDateUTC:
3207                body["expireDate"] = expDateUTC
3208
3209            self.body = str(body)
3210            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0, debug=False)
3211
3212            if "stopOrderId" in response.keys():
3213                uLogger.info(
3214                    "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format(
3215                        response["stopOrderId"],
3216                        self.ticker, self.figi,
3217                        operation, lots,
3218                        targetPrice, instrument["currency"],
3219                        limitPrice, instrument["currency"],
3220                        TKS_STOP_ORDER_TYPES[stopOrderType],
3221                        datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"],
3222                    ))
3223
3224                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3225                    if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3226                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3227                            targetPrice, instrument["currency"],
3228                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3229                        ))
3230
3231                    if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3232                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3233                            targetPrice, instrument["currency"],
3234                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3235                        ))
3236
3237            else:
3238                uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log or try again and open order later.")
3239
3240        return response

Universal method to create market or limit orders with all available parameters for current accountId. See more simple methods: BuyLimit(), BuyStop(), SellLimit(), SellStop().

If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.

Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" then broker immediately open market order as you can do simple --buy or --sell operations!

If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". When current price will go up or down to target price value then broker opens a limit order. Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.

Only one attempt and no retry for opens order. If network issue occurred you can create new request.

Parameters
  • operation: string "Buy" or "Sell".
  • orderType: string "Limit" or "Stop".
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is open trade price for limit order.
  • limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
  • stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. Stop loss order always executed by market price.
  • expDate: string "Undefined" by default or local date in future. String has a format like this: %Y-%m-%d %H:%M:%S. This date is converting to UTC format for server. This parameter only makes sense for stop-order. A limit order has no expiration date, it lasts until the end of the trading day.
Returns

JSON with response from broker server.

def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3242    def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3243        """
3244        Create pending `Buy` limit-order (below current price). You must specify only 2 parameters:
3245        `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then
3246        broker immediately open `Buy` market order, such as if you do simple `--buy` operation!
3247        See also: `Order()` docstring.
3248
3249        :param lots: volume, integer count of lots >= 1.
3250        :param targetPrice: target price > 0. This is open trade price for limit order.
3251        :return: JSON with response from broker server.
3252        """
3253        return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)

Create pending Buy limit-order (below current price). You must specify only 2 parameters: lots and target price to open buy limit-order. If you try to create buy limit-order above current price then broker immediately open Buy market order, such as if you do simple --buy operation! See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is open trade price for limit order.
Returns

JSON with response from broker server.

def BuyStop( self, lots: int, targetPrice: float, limitPrice: float = 0.0, stopType: str = 'Limit', expDate: str = 'Undefined') -> dict:
3255    def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3256        """
3257        Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order.
3258        In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3259        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3260        target price value then broker opens a limit order. See also: `Order()` docstring.
3261
3262        :param lots: volume, integer count of lots >= 1.
3263        :param targetPrice: target price > 0. This is trigger price for buy stop-order.
3264        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3265                           with price equal to limitPrice, when current price goes to target price of buy stop-order.
3266        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3267                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3268        :param expDate: string "Undefined" by default or local date in future.
3269                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3270                        This date is converting to UTC format for server.
3271        :return: JSON with response from broker server.
3272        """
3273        return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)

Create Buy stop-order. You must specify at least 2 parameters: lots target price to open buy stop-order. In additional you can specify 3 parameters for buy stop-order: limit price >=0, stop type = Limit|SL|TP, expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to target price value then broker opens a limit order. See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is trigger price for buy stop-order.
  • limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of buy stop-order.
  • stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
  • expDate: string "Undefined" by default or local date in future. String has a format like this: %Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns

JSON with response from broker server.

def SellLimit(self, lots: int, targetPrice: float) -> dict:
3275    def SellLimit(self, lots: int, targetPrice: float) -> dict:
3276        """
3277        Create pending `Sell` limit-order (above current price). You must specify only 2 parameters:
3278        `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then
3279        broker immediately open `Sell` market order, such as if you do simple `--sell` operation!
3280        See also: `Order()` docstring.
3281
3282        :param lots: volume, integer count of lots >= 1.
3283        :param targetPrice: target price > 0. This is open trade price for limit order.
3284        :return: JSON with response from broker server.
3285        """
3286        return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)

Create pending Sell limit-order (above current price). You must specify only 2 parameters: lots and target price to open sell limit-order. If you try to create sell limit-order below current price then broker immediately open Sell market order, such as if you do simple --sell operation! See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is open trade price for limit order.
Returns

JSON with response from broker server.

def SellStop( self, lots: int, targetPrice: float, limitPrice: float = 0.0, stopType: str = 'Limit', expDate: str = 'Undefined') -> dict:
3288    def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3289        """
3290        Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order.
3291        In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3292        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3293        target price value then broker opens a limit order. See also: `Order()` docstring.
3294
3295        :param lots: volume, integer count of lots >= 1.
3296        :param targetPrice: target price > 0. This is trigger price for sell stop-order.
3297        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3298                           with price equal to limitPrice, when current price goes to target price of sell stop-order.
3299        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3300                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3301        :param expDate: string "Undefined" by default or local date in future.
3302                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3303                        This date is converting to UTC format for server.
3304        :return: JSON with response from broker server.
3305        """
3306        return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)

Create Sell stop-order. You must specify at least 2 parameters: lots target price to open sell stop-order. In additional you can specify 3 parameters for sell stop-order: limit price >=0, stop type = Limit|SL|TP, expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to target price value then broker opens a limit order. See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is trigger price for sell stop-order.
  • limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of sell stop-order.
  • stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
  • expDate: string "Undefined" by default or local date in future. String has a format like this: %Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns

JSON with response from broker server.

def CloseOrders( self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3308    def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3309        """
3310        Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`.
3311
3312        :param orderIDs: list of integers with `orderId` or `stopOrderId`.
3313        :param allOrdersIDs: pre-received lists of all active pending orders.
3314                             This avoids unnecessary downloading data from the server.
3315        :param allStopOrdersIDs: pre-received lists of all active stop orders.
3316        """
3317        if self.accountId is None or not self.accountId:
3318            uLogger.error("Variable `accountId` must be defined for using this method!")
3319            raise Exception("Account ID required")
3320
3321        if orderIDs:
3322            if allOrdersIDs is None or not allOrdersIDs:
3323                rawOrders = self.RequestPendingOrders()
3324                allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending orders ID
3325
3326            if allStopOrdersIDs is None or not allStopOrdersIDs:
3327                rawStopOrders = self.RequestStopOrders()
3328                allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3329
3330            for orderID in orderIDs:
3331                idInPendingOrders = orderID in allOrdersIDs
3332                idInStopOrders = orderID in allStopOrdersIDs
3333
3334                if not (idInPendingOrders or idInStopOrders):
3335                    uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID))
3336                    continue
3337
3338                else:
3339                    if idInPendingOrders:
3340                        uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID))
3341
3342                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder
3343                        self.body = str({"accountId": self.accountId, "orderId": orderID})
3344                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder"
3345                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3346
3347                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3348                            uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3349                            uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID))
3350
3351                        else:
3352                            uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID))
3353
3354                    elif idInStopOrders:
3355                        uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID))
3356
3357                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder
3358                        self.body = str({"accountId": self.accountId, "stopOrderId": orderID})
3359                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder"
3360                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3361
3362                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3363                            uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3364                            uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID))
3365
3366                        else:
3367                            uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID))
3368
3369                    else:
3370                        continue

Cancel order or list of orders by its orderId or stopOrderId for current accountId.

Parameters
  • orderIDs: list of integers with orderId or stopOrderId.
  • allOrdersIDs: pre-received lists of all active pending orders. This avoids unnecessary downloading data from the server.
  • allStopOrdersIDs: pre-received lists of all active stop orders.
def CloseAllOrders(self) -> None:
3372    def CloseAllOrders(self) -> None:
3373        """
3374        Gets a list of open pending and stop orders and cancel it all.
3375        """
3376        rawOrders = self.RequestPendingOrders()
3377        allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending orders ID
3378        lenOrders = len(allOrdersIDs)
3379
3380        rawStopOrders = self.RequestStopOrders()
3381        allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3382        lenSOrders = len(allStopOrdersIDs)
3383
3384        if lenOrders > 0 or lenSOrders > 0:
3385            uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders))
3386
3387            self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs)
3388
3389        else:
3390            uLogger.info("Orders not found, nothing to cancel.")

Gets a list of open pending and stop orders and cancel it all.

def CloseAll(self, *args) -> None:
3392    def CloseAll(self, *args) -> None:
3393        """
3394        Close all available (not blocked) opened trades and orders.
3395
3396        Also, you can select one or more keywords case-insensitive:
3397        `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type.
3398
3399        Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods.
3400        """
3401        overview = self.Overview(show=False)  # get all open trades info
3402
3403        if len(args) == 0:
3404            uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...")
3405            self.CloseAllOrders()  # close all pending and stop orders
3406
3407            for iType in TKS_INSTRUMENTS:
3408                if iType != "Currencies":
3409                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3410
3411        else:
3412            uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args)))
3413            lowerArgs = [x.lower() for x in args]
3414
3415            if "orders" in lowerArgs:
3416                self.CloseAllOrders()  # close all pending and stop orders
3417
3418            for iType in TKS_INSTRUMENTS:
3419                if iType.lower() in lowerArgs and iType != "Currencies":
3420                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies

Close all available (not blocked) opened trades and orders.

Also, you can select one or more keywords case-insensitive: orders, shares, bonds, etfs and futures from TKS_INSTRUMENTS enum to specify trades type.

Currency positions you must close manually using buy or sell operations, CloseTrades() or CloseAllTrades() methods.

@staticmethod
def ParseOrderParameters(operation, **inputParameters)
3422    @staticmethod
3423    def ParseOrderParameters(operation, **inputParameters):
3424        """
3425        Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
3426
3427        :param operation: string "Buy" or "Sell".
3428        :param inputParameters: this is dict of strings that looks like this
3429               `{"lots": "L_int,...", "prices": "P_float,..."}` where
3430               "lots" key: one or more lot values (integer numbers) to open with every limit-order
3431               "prices" key: one or more prices to open limit-orders
3432               Counts of values in lots and prices lists must be equals!
3433        :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]`
3434        """
3435        # TODO: update order grid work with api v2
3436        pass
3437        # uLogger.debug("Input parameters: {}".format(inputParameters))
3438        #
3439        # if operation is None or not operation or operation not in ("Buy", "Sell"):
3440        #     uLogger.error("You must define operation type: 'Buy' or 'Sell'!")
3441        #     raise Exception("Incorrect value")
3442        #
3443        # if "l" in inputParameters.keys():
3444        #     inputParameters["lots"] = inputParameters.pop("l")
3445        #
3446        # if "p" in inputParameters.keys():
3447        #     inputParameters["prices"] = inputParameters.pop("p")
3448        #
3449        # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys():
3450        #     uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!")
3451        #     raise Exception("Incorrect value")
3452        #
3453        # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")]
3454        # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")]
3455        #
3456        # if len(lots) != len(prices):
3457        #     uLogger.error("'lots' and 'prices' lists must have equal length of values!")
3458        #     raise Exception("Incorrect value")
3459        #
3460        # uLogger.debug("Extracted parameters for orders:")
3461        # uLogger.debug("lots = {}".format(lots))
3462        # uLogger.debug("prices = {}".format(prices))
3463        #
3464        # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...]
3465        # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))]
3466        # uLogger.debug("Order parameters: {}".format(result))
3467        #
3468        # return result

Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.

Parameters
  • operation: string "Buy" or "Sell".
  • inputParameters: this is dict of strings that looks like this {"lots": "L_int,...", "prices": "P_float,..."} where "lots" key: one or more lot values (integer numbers) to open with every limit-order "prices" key: one or more prices to open limit-orders Counts of values in lots and prices lists must be equals!
Returns

list of dictionaries with all lots and prices to open orders that looks like this [{"lot": lots_1, "price": price_1}, {...}, ...]

def IsInPortfolio(self, portfolio: dict = None) -> bool:
3470    def IsInPortfolio(self, portfolio: dict = None) -> bool:
3471        """
3472        Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`.
3473
3474        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3475        :return: `True` if portfolio contains open position with given instrument, `False` otherwise.
3476        """
3477        result = False
3478        msg = "Instrument not defined!"
3479
3480        if portfolio is None or not portfolio:
3481            portfolio = self.Overview(show=False)
3482
3483        if self.ticker:
3484            uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker))
3485            msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker)
3486
3487            for iType in TKS_INSTRUMENTS:
3488                for instrument in portfolio["stat"][iType]:
3489                    if instrument["ticker"] == self.ticker:
3490                        result = True
3491                        msg = "Instrument with ticker [{}] is present in open positions".format(self.ticker)
3492                        break
3493
3494        elif self.figi:
3495            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi))
3496            msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi)
3497
3498            for iType in TKS_INSTRUMENTS:
3499                for instrument in portfolio["stat"][iType]:
3500                    if instrument["figi"] == self.figi:
3501                        result = True
3502                        msg = "Instrument with FIGI [{}] is present in open positions".format(self.figi)
3503                        break
3504
3505        else:
3506            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3507
3508        uLogger.debug(msg)
3509
3510        return result

Checks if instrument is in the user's portfolio. Instrument must be defined by ticker (highly priority) or figi.

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

True if portfolio contains open position with given instrument, False otherwise.

def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3512    def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3513        """
3514        Returns instrument is in the user's portfolio if it presents there.
3515        Instrument must be defined by `ticker` (highly priority) or `figi`.
3516
3517        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3518        :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise.
3519        """
3520        result = None
3521        msg = "Instrument not defined!"
3522
3523        if portfolio is None or not portfolio:
3524            portfolio = self.Overview(show=False)
3525
3526        if self.ticker:
3527            uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker))
3528            msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker)
3529
3530            for iType in TKS_INSTRUMENTS:
3531                for instrument in portfolio["stat"][iType]:
3532                    if instrument["ticker"] == self.ticker:
3533                        result = instrument
3534                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self.ticker, instrument["figi"])
3535                        break
3536
3537        elif self.figi:
3538            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi))
3539            msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi)
3540
3541            for iType in TKS_INSTRUMENTS:
3542                for instrument in portfolio["stat"][iType]:
3543                    if instrument["figi"] == self.figi:
3544                        result = instrument
3545                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self.figi)
3546                        break
3547
3548        else:
3549            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3550
3551        uLogger.debug(msg)
3552
3553        return result

Returns instrument is in the user's portfolio if it presents there. Instrument must be defined by ticker (highly priority) or figi.

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

dict with instrument if portfolio contains open position with this instrument, None otherwise.

def RequestLimits(self) -> dict:
3555    def RequestLimits(self) -> dict:
3556        """
3557        Method for obtaining the available funds for withdrawal for current `accountId`.
3558
3559        See also:
3560        - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
3561        - `OverviewLimits()` method
3562
3563        :return: dict with raw data from server that contains free funds for withdrawal. Example of dict:
3564                 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`.
3565                 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency
3566                 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures.
3567        """
3568        if self.accountId is None or not self.accountId:
3569            uLogger.error("Variable `accountId` must be defined for using this method!")
3570            raise Exception("Account ID required")
3571
3572        uLogger.debug("Requesting current available funds for withdrawal. Wait, please...")
3573
3574        self.body = str({"accountId": self.accountId})
3575        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits"
3576        rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3577
3578        uLogger.debug("Records about available funds for withdrawal successfully received")
3579
3580        return rawLimits

Method for obtaining the available funds for withdrawal for current accountId.

See also:

Returns

dict with raw data from server that contains free funds for withdrawal. Example of dict: {"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}. Here money is an array of portfolio currency positions, blocked is an array of blocked currency positions of the portfolio and blockedGuarantee is locked money under collateral for futures.

def OverviewLimits(self, show: bool = False) -> dict:
3582    def OverviewLimits(self, show: bool = False) -> dict:
3583        """
3584        Method for parsing and show table with available funds for withdrawal for current `accountId`.
3585
3586        See also: `RequestLimits()`.
3587
3588        :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log.
3589        :return: dict with raw parsed data from server and some calculated statistics about it.
3590        """
3591        if self.accountId is None or not self.accountId:
3592            uLogger.error("Variable `accountId` must be defined for using this method!")
3593            raise Exception("Account ID required")
3594
3595        rawLimits = self.RequestLimits()  # raw response with current available funds for withdrawal
3596
3597        view = {
3598            "rawLimits": rawLimits,
3599            "limits": {  # parsed data for every currency:
3600                "money": {  # this is an array of portfolio currency positions
3601                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"]
3602                },
3603                "blocked": {  # this is an array of blocked currency
3604                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"]
3605                },
3606                "blockedGuarantee": {  # this is locked money under collateral for futures
3607                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"]
3608                },
3609            },
3610        }
3611
3612        # --- Prepare text table with limits in human-readable format:
3613        if show:
3614            info = [
3615                "# Withdrawal limits\n\n",
3616                "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
3617                "* **Account ID:** [{}]\n".format(self.accountId),
3618                "\n| Currencies | Total         | Available for withdrawal | Blocked for trade | Futures guarantee |\n",
3619                "|------------|---------------|--------------------------|-------------------|-------------------|\n",
3620            ]
3621
3622            for curr in view["limits"]["money"].keys():
3623                blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0
3624                blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0
3625                availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee)
3626
3627                infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format(
3628                    "[{}]".format(curr),
3629                    "{:.2f}".format(view["limits"]["money"][curr]),
3630                    "{:.2f}".format(availableMoney),
3631                    "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—",
3632                    "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—",
3633                )
3634
3635                if curr == "rub":
3636                    info.insert(5, infoStr)  # insert at first position in table and after headers
3637
3638                else:
3639                    info.append(infoStr)
3640
3641            infoText = "".join(info)
3642
3643            uLogger.info(infoText)
3644
3645            if self.withdrawalLimitsFile:
3646                with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH:
3647                    fH.write(infoText)
3648
3649                uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile)))
3650
3651        return view

Method for parsing and show table with available funds for withdrawal for current accountId.

See also: RequestLimits().

Parameters
  • show: if False then only dictionary returns, if True then also print withdrawal limits to log.
Returns

dict with raw parsed data from server and some calculated statistics about it.

def RequestAccounts(self) -> dict:
3653    def RequestAccounts(self) -> dict:
3654        """
3655        Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`.
3656
3657        See also:
3658        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
3659        - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
3660        - `OverviewUserInfo()` method
3661
3662        :return: dict with raw data from server that contains accounts info. Example of dict:
3663                 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account",
3664                   "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z",
3665                   "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`.
3666                 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now.
3667        """
3668        uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...")
3669
3670        self.body = str({})
3671        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts"
3672        rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST")
3673
3674        uLogger.debug("Records about available accounts successfully received")
3675
3676        return rawAccounts

Method for requesting all brokerage accounts (accountIds) of current user detected by token.

See also:

Returns

dict with raw data from server that contains accounts info. Example of dict: {"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}. If closedDate="1970-01-01T00:00:00Z" it means that account is active now.

def RequestUserInfo(self) -> dict:
3678    def RequestUserInfo(self) -> dict:
3679        """
3680        Method for requesting common user's information.
3681
3682        See also:
3683        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
3684        - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
3685        - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with
3686        - `OverviewUserInfo()` method
3687
3688        :return: dict with raw data from server that contains user's information. Example of dict:
3689                 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage",
3690                   "russian_shares", "structured_income_bonds"], "tariff": "premium"}`.
3691        """
3692        uLogger.debug("Requesting common user's information. Wait, please...")
3693
3694        self.body = str({})
3695        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo"
3696        rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST")
3697
3698        uLogger.debug("Records about current user successfully received")
3699
3700        return rawUserInfo

Method for requesting common user's information.

See also:

Returns

dict with raw data from server that contains user's information. Example of dict: {"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", "russian_shares", "structured_income_bonds"], "tariff": "premium"}.

def RequestMarginStatus(self, accountId: str = None) -> dict:
3702    def RequestMarginStatus(self, accountId: str = None) -> dict:
3703        """
3704        Method for requesting margin calculation for defined account ID.
3705
3706        See also:
3707        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
3708        - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
3709        - `OverviewUserInfo()` method
3710
3711        :param accountId: string with numeric account ID. If `None`, then used class field `accountId`.
3712        :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict.
3713                 Example of responses:
3714                 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`.
3715                 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000},
3716                                    "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000},
3717                                    "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000},
3718                                    "fundsSufficiencyLevel": {"units": "1", "nano": 280000000},
3719                                    "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`.
3720        """
3721        if accountId is None or not accountId:
3722            if self.accountId is None or not self.accountId:
3723                uLogger.error("Variable `accountId` must be defined for using this method!")
3724                raise Exception("Account ID required")
3725
3726            else:
3727                accountId = self.accountId  # use `self.accountId` (main ID) by default
3728
3729        uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId))
3730
3731        self.body = str({"accountId": accountId})
3732        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes"
3733        rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST")
3734
3735        if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}:
3736            uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId))
3737            rawMargin = {}
3738
3739        else:
3740            uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId))
3741
3742        return rawMargin

Method for requesting margin calculation for defined account ID.

See also:

Parameters
  • accountId: string with numeric account ID. If None, then used class field accountId.
Returns

dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. Example of responses: status code 400: {"code": 3, "message": "account margin status is disabled", "description": "30051" }, returns: {}. status code 200: {"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}.

def RequestTariffLimits(self) -> dict:
3744    def RequestTariffLimits(self) -> dict:
3745        """
3746        Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`.
3747
3748        See also:
3749        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
3750        - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
3751        - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
3752        - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
3753        - `OverviewUserInfo()` method
3754
3755        :return: dict with raw data from server that contains limits of current tariff. Example of dict:
3756                 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...],
3757                   "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`.
3758        """
3759        uLogger.debug("Requesting limits of current tariff. Wait, please...")
3760
3761        self.body = str({})
3762        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff"
3763        rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3764
3765        uLogger.debug("Records with limits of current tariff successfully received")
3766
3767        return rawTariffLimits

Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by token.

See also:

Returns

dict with raw data from server that contains limits of current tariff. Example of dict: {"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}.

def RequestBondCoupons(self, iJSON: dict) -> dict:
3769    def RequestBondCoupons(self, iJSON: dict) -> dict:
3770        """
3771        Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
3772        then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`.
3773        All dates are in UTC timezone.
3774
3775        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons
3776        Documentation:
3777        - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
3778        - response: https://tinkoff.github.io/investAPI/instruments/#coupon
3779
3780        See also: `ExtendBondsData()`.
3781
3782        :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self.ticker]`
3783                      If raw iJSON is not data of bond then server returns an error [400] with message:
3784                      `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`.
3785        :return: dictionary with bond payment calendar. Response example
3786                 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12",
3787                   "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000},
3788                   "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z",
3789                   "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}`
3790        """
3791        if iJSON["figi"] is None or not iJSON["figi"]:
3792            uLogger.error("FIGI must be defined for using this method!")
3793            raise Exception("FIGI required")
3794
3795        startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z"
3796        endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z"
3797
3798        uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format(
3799            "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "",
3800            self.figi,
3801            startDate,
3802            endDate,
3803        ))
3804
3805        self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate})
3806        calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons"
3807        calendar = self.SendAPIRequest(calendarURL, reqType="POST", debug=False)
3808
3809        if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}:
3810            uLogger.warning("Instrument type is not bond!")
3811
3812        else:
3813            uLogger.debug("Records about bond payment calendar successfully received")
3814
3815        return calendar

Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown then requesting dates "from": "1970-01-01T00:00:00.000Z" and "to": "2099-12-31T23:59:59.000Z". All dates are in UTC timezone.

REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons Documentation:

See also: ExtendBondsData().

Parameters
  • iJSON: raw json data of a bond from broker server, example iJSON = self.iList["Bonds"][self.ticker] If raw iJSON is not data of bond then server returns an error [400] with message: {"code": 3, "message": "instrument type is not bond", "description": "30048"}.
Returns

dictionary with bond payment calendar. Response example {"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}

def ExtendBondsData( self, instruments: list[str], xlsx: bool = False) -> pandas.core.frame.DataFrame:
3817    def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame:
3818        """
3819        Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider
3820        pandas dataframe with more information about bonds: main info, current prices, bond payment calendar,
3821        coupon yields, current yields and some statistics etc.
3822
3823        WARNING! This is too long operation if a lot of bonds requested from broker server.
3824
3825        See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`.
3826
3827        :param instruments: list of strings with tickers or FIGIs.
3828        :param xlsx: if True then also exports pandas dataframe to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`,
3829                     for further used by data scientists or stock analytics.
3830        :return: wider pandas dataframe with more full and calculated data about bonds, than raw response from broker.
3831                 In XLSX-file and pandas dataframe fields mean:
3832                 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond
3833                 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
3834        """
3835        if instruments is None or not instruments:
3836            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
3837            raise Exception("Ticker or FIGI required")
3838
3839        if isinstance(instruments, str):
3840            instruments = [instruments]
3841
3842        uniqueInstruments = self.GetUniqueFIGIs(instruments)
3843
3844        uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...")
3845
3846        iCount = len(uniqueInstruments)
3847        tooLong = iCount >= 20
3848        if tooLong:
3849            uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...")
3850
3851        bonds = None
3852        for i, self.figi in enumerate(uniqueInstruments):
3853            instrument = self.SearchByFIGI(requestPrice=False)  # raw data about instrument from server
3854
3855            if "type" in instrument.keys() and instrument["type"] == "Bonds":
3856                # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond
3857                rawBond = self.SearchByFIGI(requestPrice=True)
3858
3859                # Widen raw data with UTC current time (iData["actualDateTime"]):
3860                actualDate = datetime.now(tzutc())
3861                iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond
3862
3863                # Widen raw data with bond payment calendar (iData["rawCalendar"]):
3864                iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)}
3865
3866                # Replace some values with human-readable:
3867                iData["nominalCurrency"] = iData["nominal"]["currency"]
3868                iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"])
3869                iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"])
3870                iData["aciCurrency"] = iData["aciValue"]["currency"]
3871                iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"])
3872                iData["issueSize"] = int(iData["issueSize"])
3873                iData["issueSizePlan"] = int(iData["issueSizePlan"])
3874                iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]]
3875                iData["step"] = iData["step"] if "step" in iData.keys() else 0
3876                iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]]
3877                iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0
3878                iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0
3879                iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0
3880                iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0
3881                iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0
3882                iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0
3883
3884                # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date):
3885                iData["limitUpPercent"] = iData["currentPrice"]["limitUp"]  # max price on current day in percents of nominal
3886                iData["limitDownPercent"] = iData["currentPrice"]["limitDown"]  # min price on current day in percents of nominal
3887                iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"]  # last price on market in percents of nominal
3888                iData["closePricePercent"] = iData["currentPrice"]["closePrice"]  # previous day close in percents of nominal
3889                iData["changes"] = iData["currentPrice"]["changes"]  # this is percent of changes between `currentPrice` and `lastPrice`
3890                iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100  # max price on current day is `limitUpPercent` * `nominal`
3891                iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100  # min price on current day is `limitDownPercent` * `nominal`
3892                iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100  # last price on market is `lastPricePercent` * `nominal`
3893                iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100  # previous day close is `closePricePercent` * `nominal`
3894                iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"]  # this is delta between last deal price and last close
3895
3896                # Widen raw data with calendar data from `rawCalendar` values:
3897                calendarData = []
3898                for item in iData["rawCalendar"]["events"]:
3899                    calendarData.append({
3900                        "couponDate": item["couponDate"],
3901                        "couponNumber": int(item["couponNumber"]),
3902                        "fixDate": item["fixDate"] if "fixDate" in item.keys() else "",
3903                        "payCurrency": item["payOneBond"]["currency"],
3904                        "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]),
3905                        "couponType": TKS_COUPON_TYPES[item["couponType"]],
3906                        "couponStartDate": item["couponStartDate"],
3907                        "couponEndDate": item["couponEndDate"],
3908                        "couponPeriod": item["couponPeriod"],
3909                    })
3910
3911                # if maturity date is unknown then uses the latest date in bond payment calendar for it:
3912                if "maturityDate" not in iData.keys():
3913                    iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else ""
3914
3915                # Widen raw data with Coupon Rate.
3916                # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%:
3917                iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData])
3918                iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData])
3919                iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0.
3920
3921                # Widen raw data with Yield to Maturity (YTM) on current date.
3922                # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%:
3923                maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None
3924                iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None
3925                iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate])
3926                iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"]  # sum of all last coupons minus current ACI value
3927                iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0.
3928
3929                iData["calendar"] = calendarData  # adds calendar at the end
3930
3931                # Remove not used data:
3932                iData.pop("uid")
3933                iData.pop("positionUid")
3934                iData.pop("currentPrice")
3935                iData.pop("rawCalendar")
3936
3937                colNames = list(iData.keys())
3938                if bonds is None:
3939                    bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames))
3940
3941                else:
3942                    bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True)
3943
3944            else:
3945                uLogger.warning("Instrument with ticker [{}] and FIGI [{}] is not a bond!".format(instrument["ticker"], instrument["figi"]))
3946
3947            processed = round(100 * (i + 1) / iCount, 1)
3948            if tooLong and processed % 5 == 0:
3949                uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount))
3950
3951            else:
3952                uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount))
3953
3954        bonds.index = bonds["ticker"].tolist()  # replace indexes with ticker names
3955
3956        # Saving bonds from pandas dataframe to XLSX sheet:
3957        if xlsx and self.bondsXLSXFile:
3958            with pd.ExcelWriter(
3959                    path=self.bondsXLSXFile,
3960                    date_format=TKS_DATE_FORMAT,
3961                    datetime_format=TKS_DATE_TIME_FORMAT,
3962                    mode="w",
3963            ) as writer:
3964                bonds.to_excel(
3965                    writer,
3966                    sheet_name="Extended bonds data",
3967                    index=True,
3968                    encoding="UTF-8",
3969                    freeze_panes=(1, 1),
3970                )  # saving as XLSX-file with freeze first row and column as headers
3971
3972            uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile)))
3973
3974        return bonds

Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider pandas dataframe with more information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc.

WARNING! This is too long operation if a lot of bonds requested from broker server.

See also: ShowInstrumentInfo(), CreateBondsCalendar(), ShowBondsCalendar(), RequestBondCoupons().

Parameters
  • instruments: list of strings with tickers or FIGIs.
  • xlsx: if True then also exports pandas dataframe to xlsx-file bondsXLSXFile, default ext-bonds.xlsx, for further used by data scientists or stock analytics.
Returns

wider pandas dataframe with more full and calculated data about bonds, than raw response from broker. In XLSX-file and pandas dataframe fields mean: - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon

def CreateBondsCalendar( self, extBonds: pandas.core.frame.DataFrame, xlsx: bool = False) -> pandas.core.frame.DataFrame:
3976    def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame:
3977        """
3978        Creates bond payments calendar as pandas dataframe, and also save it to the XLSX-file, `calendar.xlsx` by default.
3979
3980        WARNING! This is too long operation if a lot of bonds requested from broker server.
3981
3982        See also: `ShowBondsCalendar()`, `ExtendBondsData()`.
3983
3984        :param extBonds: pandas dataframe object returns by `ExtendBondsData()` method and contains
3985                        extended information about bonds: main info, current prices, bond payment calendar,
3986                        coupon yields, current yields and some statistics etc.
3987                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
3988        :param xlsx: if True then also exports pandas dataframe to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default,
3989                     for further used by data scientists or stock analytics.
3990        :return: pandas dataframe with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
3991        """
3992        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
3993            extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False)
3994
3995        uLogger.debug("Generating bond payments calendar data. Wait, please...")
3996
3997        colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"]
3998        colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"]
3999        calendar = None
4000        for bond in extBonds.iterrows():
4001            for item in bond[1]["calendar"]:
4002                cData = {
4003                    "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()),
4004                    "couponDate": item["couponDate"],
4005                    "figi": bond[1]["figi"],
4006                    "ticker": bond[1]["ticker"],
4007                    "name": bond[1]["name"],
4008                    "couponNumber": item["couponNumber"],
4009                    "payOneBond": item["payOneBond"],
4010                    "payCurrency": item["payCurrency"],
4011                    "couponType": item["couponType"],
4012                    "couponPeriod": item["couponPeriod"],
4013                    "fixDate": item["fixDate"],
4014                    "couponStartDate": item["couponStartDate"],
4015                    "couponEndDate": item["couponEndDate"],
4016                }
4017
4018                if calendar is None:
4019                    calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID))
4020
4021                else:
4022                    calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True)
4023
4024        calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True)  # sort all payments for all bonds by payment date
4025
4026        # Saving calendar from pandas dataframe to XLSX sheet:
4027        if xlsx:
4028            xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx"
4029
4030            with pd.ExcelWriter(
4031                    path=xlsxCalendarFile,
4032                    date_format=TKS_DATE_FORMAT,
4033                    datetime_format=TKS_DATE_TIME_FORMAT,
4034                    mode="w",
4035            ) as writer:
4036                humanReadable = calendar.copy(deep=True)
4037                humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0])
4038                humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0])
4039                humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0])
4040                humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0])
4041                humanReadable.columns = colNames  # human-readable column names
4042
4043                humanReadable.to_excel(
4044                    writer,
4045                    sheet_name="Bond payments calendar",
4046                    index=False,
4047                    encoding="UTF-8",
4048                    freeze_panes=(1, 2),
4049                )  # saving as XLSX-file with freeze first row and column as headers
4050
4051                del humanReadable  # release df in memory
4052
4053            uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile)))
4054
4055        return calendar

Creates bond payments calendar as pandas dataframe, and also save it to the XLSX-file, calendar.xlsx by default.

WARNING! This is too long operation if a lot of bonds requested from broker server.

See also: ShowBondsCalendar(), ExtendBondsData().

Parameters
  • extBonds: pandas dataframe object returns by ExtendBondsData() method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter is None then used figi or ticker as bond name and then calculate ExtendBondsData().
  • xlsx: if True then also exports pandas dataframe to file calendarFile + ".xlsx", calendar.xlsx by default, for further used by data scientists or stock analytics.
Returns

pandas dataframe with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon

def ShowBondsCalendar(self, extBonds: pandas.core.frame.DataFrame, show: bool = True) -> str:
4057    def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str:
4058        """
4059        Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond.
4060        Also, creates Markdown file with calendar data, `calendar.md` by default.
4061
4062        See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`.
4063
4064        :param extBonds: pandas dataframe object returns by `ExtendBondsData()` method and contains
4065                        extended information about bonds: main info, current prices, bond payment calendar,
4066                        coupon yields, current yields and some statistics etc.
4067                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4068        :param show: if `True` then also printing bonds payment calendar to the console,
4069                     otherwise save to file `calendarFile` only. `False` by default.
4070        :return: multilines text in Markdown format with bonds payment calendar as a table.
4071        """
4072        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4073            extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False)
4074
4075        infoText = "# Bond payments calendar\n\n"
4076
4077        calendar = self.CreateBondsCalendar(extBonds, xlsx=True)  # generate pandas dataframe with full calendar data
4078
4079        if not calendar.empty:
4080            splitLine = "|       |                 |              |              |     |               |           |        |                   |\n"
4081
4082            info = [
4083                "| Paid  | Payment date    | FIGI         | Ticker       | No. | Value         | Type      | Period | End registry date |\n",
4084                "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n",
4085            ]
4086
4087            newMonth = False
4088            notOneBond = calendar["figi"].nunique() > 1
4089            for i, bond in enumerate(calendar.iterrows()):
4090                if newMonth and notOneBond:
4091                    info.append(splitLine)
4092
4093                info.append(
4094                    "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format(
4095                        "  √" if bond[1]["paid"] else "  —",
4096                        bond[1]["couponDate"].split("T")[0],
4097                        bond[1]["figi"],
4098                        bond[1]["ticker"],
4099                        bond[1]["couponNumber"],
4100                        "{} {}".format(
4101                            "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."),
4102                            bond[1]["payCurrency"],
4103                        ),
4104                        bond[1]["couponType"],
4105                        bond[1]["couponPeriod"],
4106                        bond[1]["fixDate"].split("T")[0],
4107                    )
4108                )
4109
4110                if i < len(calendar.values) - 1:
4111                    curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4112                    nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4113                    newMonth = False if curDate.month == nextDate.month else True
4114
4115                else:
4116                    newMonth = False
4117
4118            infoText += "".join(info)
4119
4120            if show:
4121                uLogger.info("{}".format(infoText))
4122
4123            if self.calendarFile is not None:
4124                with open(self.calendarFile, "w", encoding="UTF-8") as fH:
4125                    fH.write(infoText)
4126
4127                uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile)))
4128
4129        else:
4130            infoText += "No data\n"
4131
4132        return infoText

Show bond payments calendar as a table. One row in input bonds dataframe contains one bond. Also, creates Markdown file with calendar data, calendar.md by default.

See also: ShowInstrumentInfo(), RequestBondCoupons(), CreateBondsCalendar() and ExtendBondsData().

Parameters
  • extBonds: pandas dataframe object returns by ExtendBondsData() method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter is None then used figi or ticker as bond name and then calculate ExtendBondsData().
  • show: if True then also printing bonds payment calendar to the console, otherwise save to file calendarFile only. False by default.
Returns

multilines text in Markdown format with bonds payment calendar as a table.

def OverviewAccounts(self, show: bool = False) -> dict:
4134    def OverviewAccounts(self, show: bool = False) -> dict:
4135        """
4136        Method for parsing and show simple table with all available user accounts.
4137
4138        See also: `RequestAccounts()` and `OverviewUserInfo()` methods.
4139
4140        :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log.
4141        :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict:
4142                 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...},
4143                          "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1",
4144                                                        "status": "Opened and active account", "opened": "2018-05-23 00:00:00",
4145                                                        "closed": "—", "access": "Full access" }, ...}}`
4146        """
4147        rawAccounts = self.RequestAccounts()  # Raw responses with accounts
4148
4149        # This is an array of dict with user accounts, its `accountId`s and some parsed data:
4150        accounts = {
4151            item["id"]: {
4152                "type": TKS_ACCOUNT_TYPES[item["type"]],
4153                "name": item["name"],
4154                "status": TKS_ACCOUNT_STATUSES[item["status"]],
4155                "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4156                "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—",
4157                "access": TKS_ACCESS_LEVELS[item["accessLevel"]],
4158            } for item in rawAccounts["accounts"]
4159        }
4160
4161        # Raw and parsed data with some fields replaced in "stat" section:
4162        view = {
4163            "rawAccounts": rawAccounts,
4164            "stat": accounts,
4165        }
4166
4167        # --- Prepare simple text table with only accounts data in human-readable format:
4168        if show:
4169            info = [
4170                "# User accounts\n\n",
4171                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4172                "| Account ID   | Type                      | Status                    | Name                           |\n",
4173                "|--------------|---------------------------|---------------------------|--------------------------------|\n",
4174            ]
4175
4176            for account in view["stat"].keys():
4177                info.extend([
4178                    "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format(
4179                        account,
4180                        view["stat"][account]["type"],
4181                        view["stat"][account]["status"],
4182                        view["stat"][account]["name"],
4183                    )
4184                ])
4185
4186            infoText = "".join(info)
4187
4188            uLogger.info(infoText)
4189
4190            if self.userAccountsFile:
4191                with open(self.userAccountsFile, "w", encoding="UTF-8") as fH:
4192                    fH.write(infoText)
4193
4194                uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile)))
4195
4196        return view

Method for parsing and show simple table with all available user accounts.

See also: RequestAccounts() and OverviewUserInfo() methods.

Parameters
  • show: if False then only dictionary with accounts data returns, if True then also print it to log.
Returns

dict with parsed accounts data received from RequestAccounts() method. Example of dict: view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", "status": "Opened and active account", "opened": "2018-05-23 00:00:00", "closed": "—", "access": "Full access" }, ...}}

def OverviewUserInfo(self, show: bool = False) -> dict:
4198    def OverviewUserInfo(self, show: bool = False) -> dict:
4199        """
4200        Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit).
4201
4202        See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods.
4203
4204        :param show: if `False` then only dictionary returns, if `True` then also print user's data to log.
4205        :return: dict with raw parsed data from server and some calculated statistics about it.
4206        """
4207        rawUserInfo = self.RequestUserInfo()  # Raw response with common user info
4208        overviewAccount = self.OverviewAccounts(show=False)  # Raw and parsed accounts data
4209        rawAccounts = overviewAccount["rawAccounts"]  # Raw response with user accounts data
4210        accounts = overviewAccount["stat"]  # Dict with only statistics about user accounts
4211        rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()}  # Raw response with margin calculation for every account ID
4212        rawTariffLimits = self.RequestTariffLimits()  # Raw response with limits of current tariff
4213
4214        # This is dict with parsed common user data:
4215        userInfo = {
4216            "premium": "Yes" if rawUserInfo["premStatus"] else "No",
4217            "qualified": "Yes" if rawUserInfo["qualStatus"] else "No",
4218            "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]],
4219            "tariff": rawUserInfo["tariff"],
4220        }
4221
4222        # This is an array of dict with parsed margin statuses for every account IDs:
4223        margins = {}
4224        for accountId in accounts.keys():
4225            if rawMargins[accountId]:
4226                margins[accountId] = {
4227                    "currency": rawMargins[accountId]["liquidPortfolio"]["currency"],
4228                    "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]),
4229                    "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]),
4230                    "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]),
4231                    "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]),
4232                    "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]),
4233                }
4234
4235            else:
4236                margins[accountId] = {}  # Server response: margin status is disabled for current accountId
4237
4238        unary = {}  # unary-connection limits
4239        for item in rawTariffLimits["unaryLimits"]:
4240            if item["limitPerMinute"] in unary.keys():
4241                unary[item["limitPerMinute"]].extend(item["methods"])
4242
4243            else:
4244                unary[item["limitPerMinute"]] = item["methods"]
4245
4246        stream = {}  # stream-connection limits
4247        for item in rawTariffLimits["streamLimits"]:
4248            if item["limit"] in stream.keys():
4249                stream[item["limit"]].extend(item["streams"])
4250
4251            else:
4252                stream[item["limit"]] = item["streams"]
4253
4254        # This is dict with parsed limits of current tariff (connections, API methods etc.):
4255        limits = {
4256            "unary": unary,
4257            "stream": stream,
4258        }
4259
4260        # Raw and parsed data as an output result:
4261        view = {
4262            "rawUserInfo": rawUserInfo,
4263            "rawAccounts": rawAccounts,
4264            "rawMargins": rawMargins,
4265            "rawTariffLimits": rawTariffLimits,
4266            "stat": {
4267                "userInfo": userInfo,
4268                "accounts": accounts,
4269                "margins": margins,
4270                "limits": limits,
4271            },
4272        }
4273
4274        # --- Prepare text table with user information in human-readable format:
4275        if show:
4276            info = [
4277                "# Full user information\n\n",
4278                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4279                "## Common information\n\n",
4280                "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]),
4281                "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]),
4282                "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]),
4283                "* **Allowed to work with instruments:**\n{}\n".format("".join(["  - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])),
4284                "\n## User accounts\n\n",
4285            ]
4286
4287            for account in view["stat"]["accounts"].keys():
4288                info.extend([
4289                    "### ID: [{}]\n\n".format(account),
4290                    "| Parameters           | Values                                                       |\n",
4291                    "|----------------------|--------------------------------------------------------------|\n",
4292                    "| Account type:        | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]),
4293                    "| Account name:        | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]),
4294                    "| Account status:      | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]),
4295                    "| Access level:        | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]),
4296                    "| Date opened:         | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]),
4297                    "| Date closed:         | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]),
4298                ])
4299
4300                if margins[account]:
4301                    info.extend([
4302                        "| Margin status:       | Enabled                                                      |\n",
4303                        "| - Liquid portfolio:  | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])),
4304                        "| - Margin starting:   | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])),
4305                        "| - Margin minimum:    | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])),
4306                        "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)),
4307                        "| - Missing funds:     | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])),
4308                    ])
4309
4310                else:
4311                    info.append("| Margin status:       | Disabled                                                     |\n\n")
4312
4313            info.extend([
4314                "\n## Current user tariff limits\n",
4315                "\nSee also:\n",
4316                "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n",
4317                "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n",
4318                "  - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n",
4319                "  - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n",
4320                "\n### Unary limits\n",
4321            ])
4322
4323            if unary:
4324                for key, values in sorted(unary.items()):
4325                    info.append("\n* Max requests per minute: {}\n".format(key))
4326
4327                    for value in values:
4328                        info.append("  - {}\n".format(value))
4329
4330            else:
4331                info.append("\nNot available\n")
4332
4333            info.append("\n### Stream limits\n")
4334
4335            if stream:
4336                for key, values in sorted(stream.items()):
4337                    info.append("\n* Max stream connections: {}\n".format(key))
4338
4339                    for value in values:
4340                        info.append("  - {}\n".format(value))
4341
4342            else:
4343                info.append("\nNot available\n")
4344
4345            infoText = "".join(info)
4346
4347            uLogger.info(infoText)
4348
4349            if self.userInfoFile:
4350                with open(self.userInfoFile, "w", encoding="UTF-8") as fH:
4351                    fH.write(infoText)
4352
4353                uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile)))
4354
4355        return view

Method for parsing and show all available user's data (accountIds, common user information, margin status and tariff connections limit).

See also: OverviewAccounts(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits() methods.

Parameters
  • show: if False then only dictionary returns, if True then also print user's data to log.
Returns

dict with raw parsed data from server and some calculated statistics about it.

class Args:
4358class Args:
4359    """
4360    If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object.
4361    """
4362    def __init__(self, **kwargs):
4363        self.__dict__.update(kwargs)
4364
4365    def __getattr__(self, item):
4366        return None

If Main() function is imported as module, then this class used to convert arguments from **kwargs as object.

Args(**kwargs)
4362    def __init__(self, **kwargs):
4363        self.__dict__.update(kwargs)
def ParseArgs()
4369def ParseArgs():
4370    """
4371    Function get and parse command line keys.
4372
4373    See examples:
4374    - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md
4375    - in russian: https://tim55667757.github.io/TKSBrokerAPI/
4376    """
4377    parser = ArgumentParser()  # command-line string parser
4378
4379    parser.description = "TKSBrokerAPI is a python API to work with some methods of Tinkoff Open API using REST protocol. It can view history, orders and market information. Also, you can open orders and trades. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples"
4380    parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]"
4381
4382    # --- options:
4383
4384    parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the program. `False` by default.")
4385    parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/")
4386    parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.")
4387
4388    parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.")
4389    parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).")
4390
4391    parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.")
4392    parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.")
4393
4394    parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.")
4395
4396    parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.")
4397    parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.")
4398    parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.")
4399
4400    parser.add_argument("--debug-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.")
4401
4402    # --- commands:
4403
4404    parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.")
4405
4406    parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.")
4407    parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.")
4408    parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider pandas dataframe with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4409    parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.")
4410    parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!")
4411    parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4412    parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!")
4413    parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.")
4414
4415    parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.")
4416    parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.")
4417    parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.")
4418    parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.")
4419    parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.")
4420
4421    parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.")
4422    parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.")
4423    parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.")
4424    parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).")
4425
4426    parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.")
4427    parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4428    parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4429
4430    parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.")
4431    parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!")
4432    parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!")
4433    parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4434    parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4435    # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4436    # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4437
4438    parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4439    parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4440    parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` key, including for currencies tickers.")
4441    parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers, including for currencies tickers.")
4442    parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations.")
4443
4444    parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.")
4445    parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.")
4446    parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.")
4447
4448    cmdArgs = parser.parse_args()
4449    return cmdArgs

Function get and parse command line keys.

See examples:

def Main(**kwargs)
4452def Main(**kwargs):
4453    """
4454    Main function for work with Tinkoff Open API service. It realizes simple logic: get a lot of options and execute one command.
4455
4456    See examples:
4457    - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md
4458    - in russian: https://tim55667757.github.io/TKSBrokerAPI/
4459    """
4460    args = Args(**kwargs) if kwargs else ParseArgs()  # get and parse command-line parameters or use **kwarg parameters
4461
4462    if args.debug_level:
4463        uLogger.level = 10  # always debug level by default
4464        uLogger.handlers[0].level = args.debug_level  # level for STDOUT
4465
4466    exitCode = 0
4467    start = datetime.now(tzutc())
4468    uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format(
4469        start.strftime(TKS_PRINT_DATE_TIME_FORMAT),
4470        start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4471    ))
4472
4473    # trying to calculate full current version:
4474    buildVersion = __version__
4475    try:
4476        v = version("tksbrokerapi")
4477        buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0"  # set version as major.minor.dev0 if run as local build or local script
4478
4479    except Exception:
4480        buildVersion = __version__ + ".dev0"  # if an errors occurred then also set version as major.minor.dev0
4481
4482    uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion))
4483    uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT))
4484
4485    try:
4486        if args.version:
4487            print("TKSBrokerAPI {}".format(buildVersion))
4488            uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion))
4489
4490        else:
4491            # Init class for trading with Tinkoff Broker: TODO: rename `server` to `trader`
4492            server = TinkoffBrokerServer(
4493                token=args.token,
4494                accountId=args.account_id,
4495                useCache=not args.no_cache,
4496            )
4497
4498            # --- set some options:
4499
4500            if args.ticker:
4501                if args.ticker in server.aliasesKeys:
4502                    server.ticker = server.aliases[args.ticker]  # Replace some tickers with its aliases
4503
4504                else:
4505                    server.ticker = args.ticker
4506
4507            if args.figi:
4508                server.figi = args.figi
4509
4510            if args.depth is not None:
4511                server.depth = args.depth
4512
4513            # --- do one of commands:
4514
4515            if args.list:
4516                if args.output is not None:
4517                    server.instrumentsFile = args.output
4518
4519                server.ShowInstrumentsInfo(show=True)
4520
4521            elif args.list_xlsx:
4522                server.DumpInstrumentsAsXLSX(forceUpdate=False)
4523
4524            elif args.bonds_xlsx is not None:
4525                if args.output is not None:
4526                    server.bondsXLSXFile = args.output
4527
4528                if len(args.bonds_xlsx) == 0:
4529                    server.ExtendBondsData(instruments=server.iList["Bonds"].keys(), xlsx=True)  # request bonds with all available tickers
4530
4531                else:
4532                    server.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True)  # request list of given bonds
4533
4534            elif args.search:
4535                if args.output is not None:
4536                    server.searchResultsFile = args.output
4537
4538                server.SearchInstruments(pattern=args.search[0], show=True)
4539
4540            elif args.info:
4541                if not (args.ticker or args.figi):
4542                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4543                    raise Exception("Ticker or FIGI required")
4544
4545                if args.output is not None:
4546                    server.infoFile = args.output
4547
4548                if args.ticker:
4549                    server.SearchByTicker(requestPrice=True, show=True, debug=False)  # show info and current prices by ticker name
4550
4551                else:
4552                    server.SearchByFIGI(requestPrice=True, show=True, debug=False)  # show info and current prices by FIGI id
4553
4554            elif args.calendar is not None:
4555                if args.output is not None:
4556                    server.calendarFile = args.output
4557
4558                if len(args.calendar) == 0:
4559                    bondsData = server.ExtendBondsData(instruments=server.iList["Bonds"].keys(), xlsx=False)  # request bonds with all available tickers
4560
4561                else:
4562                    bondsData = server.ExtendBondsData(instruments=args.calendar, xlsx=False)  # request list of given bonds
4563
4564                server.ShowBondsCalendar(extBonds=bondsData, show=True)  # shows bonds payment calendar only
4565
4566            elif args.price:
4567                if not (args.ticker or args.figi):
4568                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4569                    raise Exception("Ticker or FIGI required")
4570
4571                server.GetCurrentPrices(show=True)
4572
4573            elif args.prices is not None:
4574                if args.output is not None:
4575                    server.pricesFile = args.output
4576
4577                server.GetListOfPrices(instruments=args.prices, show=True)  # WARNING: too long wait for a lot of instruments prices
4578
4579            elif args.overview:
4580                if args.output is not None:
4581                    server.overviewFile = args.output
4582
4583                server.Overview(show=True, details="full")
4584
4585            elif args.overview_digest:
4586                if args.output is not None:
4587                    server.overviewDigestFile = args.output
4588
4589                server.Overview(show=True, details="digest")
4590
4591            elif args.overview_positions:
4592                if args.output is not None:
4593                    server.overviewPositionsFile = args.output
4594
4595                server.Overview(show=True, details="positions")
4596
4597            elif args.overview_orders:
4598                if args.output is not None:
4599                    server.overviewOrdersFile = args.output
4600
4601                server.Overview(show=True, details="orders")
4602
4603            elif args.overview_analytics:
4604                if args.output is not None:
4605                    server.overviewAnalyticsFile = args.output
4606
4607                server.Overview(show=True, details="analytics")
4608
4609            elif args.deals is not None:
4610                if args.output is not None:
4611                    server.reportFile = args.output
4612
4613                if 0 <= len(args.deals) < 3:
4614                    server.Deals(
4615                        start=args.deals[0] if len(args.deals) >= 1 else None,
4616                        end=args.deals[1] if len(args.deals) == 2 else None,
4617                        show=True,  # Always show deals report in console
4618                        showCancelled=not args.no_cancelled,  # If --no-cancelled key then remove cancelled operations from the deals report. False by default.
4619                    )
4620
4621                else:
4622                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
4623                    raise Exception("Incorrect value")
4624
4625            elif args.history is not None:
4626                if args.output is not None:
4627                    server.historyFile = args.output
4628
4629                if 0 <= len(args.history) < 3:
4630                    dataReceived = server.History(
4631                        start=args.history[0] if len(args.history) >= 1 else None,
4632                        end=args.history[1] if len(args.history) == 2 else None,
4633                        interval="hour" if args.interval is None or not args.interval else args.interval,
4634                        onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing,
4635                        csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep,
4636                        show=True,  # shows all downloaded candles in console
4637                    )
4638
4639                    if args.render_chart is not None and dataReceived is not None:
4640                        iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
4641
4642                        server.ShowHistoryChart(
4643                            candles=dataReceived,
4644                            interact=iChart,
4645                            openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
4646                        )
4647
4648                else:
4649                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
4650                    raise Exception("Incorrect value")
4651
4652            elif args.load_history is not None:
4653                histData = server.LoadHistory(filePath=args.load_history)  # load data from file and show history in console
4654
4655                if args.render_chart is not None and histData is not None:
4656                    iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
4657                    server.ticker = os.path.basename(args.load_history)  # use filename as ticker name for PriceGenerator's chart
4658
4659                    server.ShowHistoryChart(
4660                        candles=histData,
4661                        interact=iChart,
4662                        openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
4663                    )
4664
4665            elif args.trade is not None:
4666                if 1 <= len(args.trade) <= 5:
4667                    server.Trade(
4668                        operation=args.trade[0],
4669                        lots=int(args.trade[1]) if len(args.trade) >= 2 else 1,
4670                        tp=float(args.trade[2]) if len(args.trade) >= 3 else 0.,
4671                        sl=float(args.trade[3]) if len(args.trade) >= 4 else 0.,
4672                        expDate=args.trade[4] if len(args.trade) == 5 else "Undefined",
4673                    )
4674
4675                else:
4676                    uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4677
4678            elif args.buy is not None:
4679                if 0 <= len(args.buy) <= 4:
4680                    server.Buy(
4681                        lots=int(args.buy[0]) if len(args.buy) >= 1 else 1,
4682                        tp=float(args.buy[1]) if len(args.buy) >= 2 else 0.,
4683                        sl=float(args.buy[2]) if len(args.buy) >= 3 else 0.,
4684                        expDate=args.buy[3] if len(args.buy) == 4 else "Undefined",
4685                    )
4686
4687                else:
4688                    uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4689
4690            elif args.sell is not None:
4691                if 0 <= len(args.sell) <= 4:
4692                    server.Sell(
4693                        lots=int(args.sell[0]) if len(args.sell) >= 1 else 1,
4694                        tp=float(args.sell[1]) if len(args.sell) >= 2 else 0.,
4695                        sl=float(args.sell[2]) if len(args.sell) >= 3 else 0.,
4696                        expDate=args.sell[3] if len(args.sell) == 4 else "Undefined",
4697                    )
4698
4699                else:
4700                    uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4701
4702            elif args.order:
4703                if 4 <= len(args.order) <= 7:
4704                    server.Order(
4705                        operation=args.order[0],
4706                        orderType=args.order[1],
4707                        lots=int(args.order[2]),
4708                        targetPrice=float(args.order[3]),
4709                        limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0.,
4710                        stopType=args.order[5] if len(args.order) >= 6 else "Limit",
4711                        expDate=args.order[6] if len(args.order) == 7 else "Undefined",
4712                    )
4713
4714                else:
4715                    uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`")
4716
4717            elif args.buy_limit:
4718                server.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1])
4719
4720            elif args.sell_limit:
4721                server.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1])
4722
4723            elif args.buy_stop:
4724                if 2 <= len(args.buy_stop) <= 7:
4725                    server.BuyStop(
4726                        lots=int(args.buy_stop[0]),
4727                        targetPrice=float(args.buy_stop[1]),
4728                        limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0.,
4729                        stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit",
4730                        expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined",
4731                    )
4732
4733                else:
4734                    uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4735
4736            elif args.sell_stop:
4737                if 2 <= len(args.sell_stop) <= 7:
4738                    server.SellStop(
4739                        lots=int(args.sell_stop[0]),
4740                        targetPrice=float(args.sell_stop[1]),
4741                        limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0.,
4742                        stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit",
4743                        expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined",
4744                    )
4745
4746                else:
4747                    uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help")
4748
4749            # elif args.buy_order_grid is not None:
4750            #     # update order grid work with api v2
4751            #     if len(args.buy_order_grid) == 2:
4752            #         orderParams = server.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid))
4753            #
4754            #         for order in orderParams:
4755            #             server.Order(operation="Buy", lots=order["lot"], price=order["price"])
4756            #
4757            #     else:
4758            #         uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
4759            #
4760            # elif args.sell_order_grid is not None:
4761            #     # update order grid work with api v2
4762            #     if len(args.sell_order_grid) >= 2:
4763            #         orderParams = server.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid))
4764            #
4765            #         for order in orderParams:
4766            #             server.Order(operation="Sell", lots=order["lot"], price=order["price"])
4767            #
4768            #     else:
4769            #         uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
4770
4771            elif args.close_order is not None:
4772                server.CloseOrders(args.close_order)  # close only one order
4773
4774            elif args.close_orders is not None:
4775                server.CloseOrders(args.close_orders)  # close list of orders
4776
4777            elif args.close_trade:
4778                if not args.ticker:
4779                    uLogger.error("`--ticker` key is required for this operation!")
4780                    raise Exception("Ticker required")
4781
4782                server.CloseTrades([args.ticker])  # close only one trade
4783
4784            elif args.close_trades is not None:
4785                server.CloseTrades(args.close_trades)  # close trades for list of tickers
4786
4787            elif args.close_all is not None:
4788                server.CloseAll(*args.close_all)
4789
4790            elif args.limits:
4791                if args.output is not None:
4792                    server.withdrawalLimitsFile = args.output
4793
4794                server.OverviewLimits(show=True)
4795
4796            elif args.user_info:
4797                if args.output is not None:
4798                    server.userInfoFile = args.output
4799
4800                server.OverviewUserInfo(show=True)
4801
4802            elif args.account:
4803                if args.output is not None:
4804                    server.userAccountsFile = args.output
4805
4806                server.OverviewAccounts(show=True)
4807
4808            else:
4809                uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.")
4810                raise Exception("There is no command to execute")
4811
4812    except Exception:
4813        trace = tb.format_exc()
4814        for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]:
4815            if e in trace:
4816                uLogger.error("Check your Internet connection! Failed to establish connection to broker server!")
4817                break
4818
4819        uLogger.debug(trace)
4820        uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues")
4821        exitCode = 255  # an error occurred, must be open a ticket for this issue
4822
4823    finally:
4824        finish = datetime.now(tzutc())
4825
4826        if exitCode == 0:
4827            uLogger.debug("All operations were finished success (summary code is 0).")
4828
4829        else:
4830            uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format(
4831                os.path.abspath(uLog.defaultLogFile), exitCode,
4832            ))
4833
4834        uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start))
4835        uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format(
4836            finish.strftime(TKS_PRINT_DATE_TIME_FORMAT),
4837            finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4838        ))
4839
4840        if not kwargs:
4841            sys.exit(exitCode)
4842
4843        else:
4844            return exitCode

Main function for work with Tinkoff Open API service. It realizes simple logic: get a lot of options and execute one command.

See examples: